Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled

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>
This commit is contained in:
Joseph Doherty
2026-05-05 06:21:00 -04:00
parent 43733699b0
commit fe2a6db786
3849 changed files with 352975 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
# ASB / Native Integration Decision
## Decision
Use `MxAsbClient` as the preferred implementation for the regular tag data
plane. Do not embed ASB inside the current `MxNativeSession` transport. Instead,
introduce a higher-level compatibility facade that can route each MXAccess-style
operation to either ASB or native NMX behind a common server/item-handle model.
The boundary should be operation-based, not inheritance-based:
- ASB owns direct `IASBIDataV2` connect, register, unregister, read, write,
write-completion polling, publish subscriptions, cleanup, and explicit
reconnect.
- Native NMX owns COM/DCOM activation, NMX service registration, callback export,
Galaxy repository metadata resolution, native subscription callbacks, native
recovery replay, and MXAccess behaviors whose ASB equivalent is not proven.
- The existing `MxNativeClient` should remain available as the native fallback
and diagnostic path. ASB can replace its basic scalar/array read/write and
simple data-change usage only after a shared facade makes routing explicit.
This avoids coupling the WCF ASB channel lifecycle to the DCE/RPC callback
lifecycle while still letting callers move to the simpler pure-managed x64 ASB
path where evidence is strongest.
## Evidence
Current ASB evidence is strong for the normal data path:
- The pure .NET 10 x64 ASB client connects to the live `IASBIDataV2` net.tcp
endpoint, authenticates from local DPAPI-protected solution material, and
performs register/read/write without AVEVA assembly references.
- ASB register/read/write is live-proven for deployed scalar and array tags, and
`PublishWriteComplete` supplies write-handle completion records for basic
writes.
- ASB subscriptions can create subscriptions, add monitored items, publish
values, map quality/status payloads, and raise an MXAccess-like data-change
event through `MxAsbCompatibilityServer`.
- ASB now has public option objects for connection, write, write completion,
subscriptions, monitored items, cleanup, and reconnect.
Current native evidence is still required for legacy semantics:
- `MxNativeSession` owns NMX service activation, DCE/RPC transport, callback sink
registration, heartbeat/recovery, Galaxy metadata resolution, NMX write
message projection, `Write2`, `WriteSecured2`, supervisory advise, buffered
registration, and callback parsing.
- `MxNativeCompatibilityServer` models the fuller MXAccess surface:
`AddItem2`, `AuthenticateUser`, `ArchestrAUserToId`, `Suspend`, `Activate`,
`AdviseSupervisory`, `OnWriteComplete`, `OperationComplete`, and
`OnBufferedDataChange`.
- Direct `IASBIDataV2` exposes no activate, suspend, or generic
`OperationComplete` operation. It only exposes write-completion polling.
- ASB buffered monitored items are accepted by the observed provider, but publish
still returns one current value rather than a native buffered sample batch.
- Direct system-auth ASB writes did not reproduce the public MXAccess secured
write rejection path on this VM.
## Recommended Boundary
Create a routing layer above both clients. It should hold compatibility
server/item handles and choose a backend per operation:
- `Register`, `Unregister`, `AddItem`, `RemoveItem`, `Read`, basic `Write`,
simple `Advise`, `UnAdvise`, and write-completion polling should default to
ASB when an ASB endpoint is configured and the tag/value shape is supported.
- `Write2`, `WriteSecured2`, `AuthenticateUser`, `ArchestrAUserToId`, `Suspend`,
`Activate`, `AdviseSupervisory`, buffered items, and native callback-only
events should route to native NMX until direct ASB evidence exists.
- `OperationComplete` should not be synthesized from ASB write completion. ASB
write completion can raise a write-complete event, but generic operation
completion remains native-only until a native trigger and payload mapping are
proven.
- The facade should expose which backend handled an item or operation so tests,
probes, logs, and callers can distinguish ASB behavior from native fallback.
## What ASB Should Own
- Endpoint discovery/configuration inputs for the direct `IASBIDataV2` route.
- Connection/authentication, request signing, channel cleanup, reconnect, and
connection-failure cleanup for ASB.
- ASB item identity creation, register/unregister/read, variant
encode/decode, and supported scalar/array writes.
- `AsbWriteOptions` and `AsbWriteCompletionOptions` driven write completion,
including delayed readback when requested.
- ASB subscription creation, monitored item creation/deletion, publish polling,
quality/status mapping, and basic MXAccess-compatible data-change adaptation.
- ASB-specific result/status summaries that preserve raw provider status while
exposing named categories for known cases.
## What Native NMX Should Still Own
- DCE/RPC, COM object reference handling, NTLM/SSPI authentication, callback
export, and NMX service registration.
- Galaxy repository tag resolution and metadata needed for NMX reference handles,
value-kind projection, and native buffered registration.
- `Write2` and `WriteSecured2` until ASB timestamped and secured-write parity is
explicitly proven.
- `Suspend`, `Activate`, supervisory advise, native recovery replay, heartbeat
policy, and callback-derived event surfaces.
- `OnBufferedDataChange` and generic `OperationComplete` until ASB produces
equivalent runtime evidence.
- Native diagnostic/probe coverage for payloads that ASB does not expose.
## Risks And Unknowns
- ASB endpoint discovery/configuration is environment-specific; fallback to NMX
must be explicit when no usable endpoint is configured.
- ASB access-denied, no-communication, and other live fault status cases still
need safe source conditions before they can be treated as complete parity.
- The exact native completion-only byte-to-status mapping is not fully proven.
- ASB buffered publish behavior may vary by provider or configuration; the
observed provider did not produce multi-sample batches.
- Dual routing can create semantic drift if the facade hides backend choice.
Backend selection must be visible in diagnostics and test assertions.
- Mixing ASB and native subscriptions for the same logical item can duplicate
callbacks or produce ordering differences; route each item to one backend at a
time.
## Staged Migration
1. Add a small shared facade project or class that exposes the MXAccess-style
handle API and delegates to `MxAsbCompatibilityServer` or
`MxNativeCompatibilityServer`.
2. Start with opt-in ASB routing for basic register/read/write/simple-advise
flows. Keep native as the default fallback for unsupported operations.
3. Add route annotations to events and probe output so every data-change,
write-complete, and fallback decision identifies `asb` or `native`.
4. Port basic compatibility tests to run against the facade with ASB-for-data
enabled and native fallback enabled. Preserve existing ASB-only and
native-only tests.
5. Gate any additional replacement on evidence: timestamped ASB write behavior,
secured-write behavior, buffered sample batches, and native
operation-complete parity should each require a focused capture or live probe
before moving from native to ASB.
6. After the facade is stable, treat `MxNativeClient` as the legacy/native
adapter and `MxAsbClient` as the primary data adapter. Avoid merging their
transport internals.
+222
View File
@@ -0,0 +1,222 @@
# ASB Variant Wire Format
This note documents the ASB `Variant` and related value/status payloads that
`MxAsbClient` currently understands. It is based on
`src/MxAsbClient/AsbContracts.cs`, `MxAsbDataClient.DecodeVariant`,
`MxAsbDataClient.FormatVariant`, `src/MxAsbClient.Tests/Program.cs`, and the
remaining-work notes.
## ASBIData Framing
ASB custom data-contract values are serialized as an `ASBIData` XML element
containing base64-encoded binary. The binary uses the .NET `BinaryWriter` /
`BinaryReader` primitive layout used by the client on Windows:
| Field kind | Binary layout |
| --- | --- |
| `ushort` | 2-byte little-endian unsigned integer |
| `int` / `uint` | 4-byte little-endian integer |
| `long` / `ulong` | 8-byte little-endian integer |
| `float` / `double` | 4-byte / 8-byte IEEE payload from `BitConverter` |
| `bool` fields in custom structs | one `BinaryWriter.Write(bool)` byte |
| UTF-16 strings in custom structs | 4-byte byte length, then UTF-16LE bytes |
| custom arrays | 4-byte element count, then each element's custom binary body |
The `Variant` body itself is:
| Offset | Size | Field | Notes |
| --- | ---: | --- | --- |
| 0 | 2 | `Type` | `AsbDataType` numeric ID. |
| 2 | 4 | `Length` | Logical payload length. Factory-created variants set this to `Payload.Length`. |
| 6 | 4 | `PayloadLength` | Actual byte count emitted before `Payload`. |
| 10 | `PayloadLength` | `Payload` | Type-specific bytes. Empty or missing payload is treated as empty. |
`DecodeVariant` trusts the actual payload bytes for parsing. Unsupported type IDs
with non-empty payloads are returned as raw `byte[]`; unsupported empty variants
return `null`, except known empty string/array types, which return empty values.
## Declared Type IDs
These IDs are declared by `AsbDataType`. Only the layouts listed as
decode-supported below should be considered implemented behavior.
| ID | Name | Current handling |
| ---: | --- | --- |
| 0 | `TypeByte` | Declared only; raw bytes if received. |
| 1 | `TypeChar` | Declared only; raw bytes if received. |
| 2 | `TypeInt16` | Declared only; raw bytes if received. |
| 3 | `TypeUInt16` | Declared only; raw bytes if received. |
| 4 | `TypeInt32` | Decode/write supported and live-proven. |
| 5 | `TypeUInt32` | Declared only; raw bytes if received. |
| 6 | `TypeInt64` | Declared only; raw bytes if received. |
| 7 | `TypeUInt64` | Declared only; raw bytes if received. |
| 8 | `TypeFloat` | Decode/write supported and live-proven. |
| 9 | `TypeDouble` | Decode/write supported and live-proven. |
| 10 | `TypeString` | Decode/write supported and live-proven. |
| 11 | `TypeDateTime` | Decode/write supported and live-proven. |
| 12 | `TypeDuration` | Decode/write supported and live-proven. |
| 13 | `TypeGuid` | Declared only; raw bytes if received. |
| 14 | `TypeByteString` | Declared only; raw bytes if received. |
| 15 | `TypeLocaleId` | Declared only; raw bytes if received. |
| 16 | `TypeLocalizedText` | Declared only; raw bytes if received. |
| 17 | `TypeBool` | Decode/write supported and live-proven. |
| 18 | `TypeSByte` | Declared only; raw bytes if received. |
| 19 | `TypeErrorStatus` | Declared only; raw bytes if received. |
| 20 | `TypeEnum` | Declared only; raw bytes if received. |
| 21 | `TypeDataType` | Declared only; raw bytes if received. |
| 22 | `TypeSecurityClassification` | Declared only; raw bytes if received. |
| 23 | `TypeDataQuality` | Declared only; raw bytes if received. |
| 40 | `TypeByteArray` | Declared only; raw bytes if received. |
| 41 | `TypeCharArray` | Declared only; raw bytes if received. |
| 42 | `TypeInt16Array` | Declared only; raw bytes if received. |
| 43 | `TypeUInt16Array` | Declared only; raw bytes if received. |
| 44 | `TypeInt32Array` | Decode/write supported and live-proven for deployed array tags. |
| 45 | `TypeUInt32Array` | Declared only; raw bytes if received. |
| 46 | `TypeInt64Array` | Declared only; raw bytes if received. |
| 47 | `TypeUInt64Array` | Declared only; raw bytes if received. |
| 48 | `TypeFloatArray` | Decode/write supported and live-proven for deployed array tags. |
| 49 | `TypeDoubleArray` | Decode/write supported and live-proven for deployed array tags. |
| 50 | `TypeStringArray` | Decode/write supported and live-proven for deployed array tags. |
| 51 | `TypeDateTimeArray` | Decode/write supported and live-proven for deployed array tags. |
| 52 | `TypeDurationArray` | Decode/write supported by tests; live coverage depends on deployed tags. |
| 53 | `TypeGuidArray` | Declared only; raw bytes if received. |
| 54 | `TypeByteStringArray` | Declared only; raw bytes if received. |
| 55 | `TypeLocaleIdArray` | Declared only; raw bytes if received. |
| 56 | `TypeLocalizedTextArray` | Declared only; raw bytes if received. |
| 57 | `TypeBoolArray` | Decode/write supported and live-proven for deployed array tags. |
| 58 | `TypeSByteArray` | Declared only; raw bytes if received. |
| 60 | `TypeEnumArray` | Declared only; raw bytes if received. |
| 61 | `TypeDataTypeArray` | Declared only; raw bytes if received. |
| 62 | `TypeSecurityClassificationArray` | Declared only; raw bytes if received. |
| 63 | `TypeDataQualityArray` | Declared only; raw bytes if received. |
| 65535 | `TypeUnknown` | Used for empty/user-data placeholders. |
## Supported Scalar Payloads
| Type | ID | Payload layout | Empty-payload decode |
| --- | ---: | --- | --- |
| `TypeBool` | 17 | one byte; non-zero is `true`, zero is `false` | `null` |
| `TypeInt32` | 4 | 4-byte little-endian signed integer | `null` |
| `TypeFloat` | 8 | 4-byte IEEE single from `BitConverter` | `null` |
| `TypeDouble` | 9 | 8-byte IEEE double from `BitConverter` | `null` |
| `TypeString` | 10 | UTF-16LE bytes, no per-string length inside the variant payload | empty string |
| `TypeDateTime` | 11 | 8-byte signed Windows FILETIME from `DateTime.ToFileTimeUtc()` | `null` |
| `TypeDuration` | 12 | 8-byte signed .NET tick count from `TimeSpan.Ticks` | `null` |
`FormatVariant` renders scalar values as invariant strings. `DateTime` values
format with the round-trip `O` pattern after `DateTime.FromFileTimeUtc()`;
`Duration` values format with the constant `c` `TimeSpan` pattern.
## Supported Array Payloads
Variant arrays do not carry an element count in a common header. The element
count is inferred from payload byte length for fixed-width arrays, or by walking
length-prefixed records for string arrays.
| Type | ID | Payload layout | Empty-payload decode |
| --- | ---: | --- | --- |
| `TypeInt32Array` | 44 | packed 4-byte little-endian signed integers | empty `int[]` |
| `TypeBoolArray` | 57 | one byte per element; non-zero is `true` | empty `bool[]` |
| `TypeFloatArray` | 48 | packed 4-byte IEEE singles | empty `float[]` |
| `TypeDoubleArray` | 49 | packed 8-byte IEEE doubles | empty `double[]` |
| `TypeStringArray` | 50 | repeated 4-byte byte length, then UTF-16LE bytes; zero length is empty string | empty `string[]` |
| `TypeDateTimeArray` | 51 | packed 8-byte Windows FILETIME values | empty `DateTime[]` |
| `TypeDurationArray` | 52 | packed 8-byte .NET tick counts | empty `TimeSpan[]` |
For malformed string-array payloads, decoding stops when the next declared
string byte length is negative or extends beyond the remaining payload. The
partial values decoded before that point are preserved.
## Timestamp and Duration Handling
There are two different timestamp encodings:
| Location | Encoding |
| --- | --- |
| `Variant` `TypeDateTime` / `TypeDateTimeArray` payloads | Windows FILETIME UTC values (`DateTime.ToFileTimeUtc()` / `DateTime.FromFileTimeUtc()`). |
| `RuntimeValue.Timestamp` in read/publish wrappers | 8-byte `DateTime.ToBinary()` value followed by a one-byte `TimestampSpecified` flag. Publish mapping normalizes the timestamp to UTC for `AsbPublishedValue.TimestampUtc`. |
`TypeDuration` and `TypeDurationArray` payloads are 8-byte `TimeSpan.Ticks`
values. No alternate duration units are inferred.
## String Encoding
Scalar `TypeString` payloads are raw UTF-16LE bytes without an inner length.
The variant `Length` and `PayloadLength` fields provide the byte count.
`TypeStringArray` payloads are a sequence of string records. Each record starts
with a 4-byte little-endian byte length followed by that many UTF-16LE bytes.
`null` and empty strings are both emitted as a zero-length string by
`AsbVariantFactory.FromStringArray`, and decode as `string.Empty`.
The same 4-byte byte-length plus UTF-16LE convention is also used by custom
struct string fields such as `ItemIdentity.Name`, but that is outside the
variant payload.
## Runtime Value, Quality, and Status
Quality is not part of a `Variant` payload. Reads and publishes wrap the variant
inside `RuntimeValue`, whose binary layout is:
| Order | Field | Binary layout |
| ---: | --- | --- |
| 1 | `Timestamp` | 8-byte `DateTime.ToBinary()` value |
| 2 | `TimestampSpecified` | one boolean byte |
| 3 | `Value` | nested `Variant` body |
| 4 | `Status` | nested `AsbStatus` body |
`AsbStatus` is:
| Order | Field | Binary layout |
| ---: | --- | --- |
| 1 | `Count` | signed byte |
| 2 | payload byte length | 4-byte integer |
| 3 | `Payload` | status element bytes |
`AsbPublishMapper.DecodeStatus` treats `Count` as the number of payload bytes
to inspect when it is positive, capped at the actual payload length. If `Count`
is zero or negative, it inspects the full payload.
Status element bytes are parsed as repeated records:
| Record part | Meaning |
| --- | --- |
| marker byte bit 0-6 | `AsbStatusElementType` ID |
| marker byte bit 7 set | element value is implicitly zero and no value bytes follow |
| marker byte bit 7 clear | next two bytes are a little-endian `ushort` value |
Known status element type IDs are:
| ID | Name |
| ---: | --- |
| 1 | `OpcDaStatus` |
| 2 | `OpcUaStatus` |
| 3 | `OpcUaVendorStatus` |
| 4 | `ScadaStatus` |
| 5 | `MxStatusCategory` |
| 6 | `MxStatusDetail` |
| 7 | `MxQuality` |
| 125 | `Reserved1Status` |
| 126 | `Reserved2Status` |
| 127 | `Reserved3Status` |
`MxQuality` is summarized by masking with `0x00C0`: `0x00C0` is good,
`0x0040` is uncertain, and `0x0000` is bad. Unknown quality values and unknown
status element types are preserved rather than guessed.
## Proven Versus Intentionally Open
Live/read/write coverage is complete for Boolean, Int32, Float, Double, String,
DateTime, Duration, and the deployed array tags called out in the remaining-work
notes. The non-live test suite also round-trips the supported factories through
`DecodeVariant`, including the duration array path.
The client intentionally does not infer layouts for declared-but-unsupported ASB
types such as GUID, byte strings, localized text, enum/data-type/security/data
quality variants, and their array forms. Those types stay as raw bytes until a
real deployed tag, capture, or contract evidence proves the payload shape.
Buffered publish batches are also intentionally not described here. The current
ASB buffered flag is exposed, but observed publish responses still carry a
single current `RuntimeValue` per item rather than a proven multi-sample batch
payload.
+975
View File
@@ -0,0 +1,975 @@
# MXAccess capture run - 2026-04-25
This run used the primary installed MXAccess interop assembly:
```text
C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll
```
The harness is in `src\MxTraceHarness` and builds as `net481` x86 because
MXAccess activates the 32-bit `LMXProxy.LMXProxyServer` COM server.
## Galaxy repository inputs
The tag selector is:
```text
analysis\sql\select_capture_tags.sql
```
It connects to the Galaxy repository described in
`C:\Users\dohertj2\Desktop\lmxopcua\gr\connectioninfo.md`:
```text
Server=localhost; Database=ZB; Integrated Security=SSPI
```
The output from this run is saved at:
```text
analysis\db\capture-tag-candidates.tsv
```
Selected test tags:
| Runtime reference | Type | Security | Notes |
| --- | --- | --- | --- |
| `TestChildObject.TestBool` | Boolean | Operate / 1 | scalar read/subscription |
| `TestChildObject.TestInt` | Integer | Operate / 1 | scalar read/subscription/write |
| `TestChildObject.TestString` | String | Operate / 1 | scalar read/subscription |
| `TestChildObject.TestStringArray[]` | String array, length 10 | Operate / 1 | array read/subscription |
| `NoSuchObject_999.NoSuchAttr` | invalid | n/a | negative resolution path |
Important naming result: array attributes must be passed to MXAccess with the
`[]` suffix. `TestChildObject.TestStringArray` produced
`MxCategoryConfigurationError`, detail `1003`; `TestChildObject.TestStringArray[]`
returned `System.String[]` with quality `192`.
## Harness behavior captured
Built command:
```text
dotnet build src\MxTraceHarness\MxTraceHarness.csproj -c Release
```
Executable:
```text
src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe
```
Scenarios now implemented:
| Scenario | High-level sequence |
| --- | --- |
| `register` | `Register`, wait, `Unregister` |
| `add-remove` | `Register`, `AddItem`, wait, `RemoveItem`, `Unregister` |
| `subscribe` | `Register`, `AddItem`, `AdviseSupervisory`, wait callbacks, `UnAdvise`, `RemoveItem`, `Unregister` |
| `write` | `Register`, `AddItem`, `AdviseSupervisory`, wait initial callback, `Write`, wait `OnWriteComplete`, cleanup |
Write precondition found in this run:
- `Write` immediately throws `E_INVALIDARG` if called after `AddItem` only.
- `Write` succeeds after `AdviseSupervisory` has established the item connection.
- The fourth `Write` argument should follow the existing bridge convention:
pass the Galaxy `security_classification` value for the attribute. For
`TestChildObject.TestInt`, that value is `1`.
Successful same-value write:
```text
captures\010-write-test-int-advised-same-value\harness.log
```
Observed sequence:
```text
mx.event.data-change: TestChildObject.TestInt = 99, quality 192
mx.write.begin: value 99, UserId/security classification 1
mx.write.end
mx.event.write-complete: MxCategoryOk, MxSourceRespondingAutomationObject, detail 0
```
## Captures produced
Every capture folder contains `harness.log`, `netsh.etl`, `network.pcapng`, and
tool stdout/stderr files unless noted.
| Folder | Result |
| --- | --- |
| `captures\001-register` | register/unregister success |
| `captures\002-add-remove-scalar` | scalar `AddItem`/`RemoveItem` success |
| `captures\003-subscribe-scalars` | bool/int/string initial data changes, quality `192` |
| `captures\004-subscribe-array-runtime-name` | array without `[]` returns configuration error detail `1003` |
| `captures\005-subscribe-array-bracketed-name` | array with `[]` returns `System.String[]`, length 10, quality `192` |
| `captures\006-add-invalid` | invalid `AddItem` still returns an item handle; no validation until advise |
| `captures\007-subscribe-invalid` | invalid subscribe returns configuration error detail `6` |
| `captures\008-write-test-int-same-value` | write without advise throws `E_INVALIDARG` using fourth arg `0` |
| `captures\009-write-test-int-same-value-security-1` | write without advise still throws `E_INVALIDARG` using fourth arg `1` |
| `captures\010-write-test-int-advised-same-value` | advised same-value write succeeds |
| `captures\011-pktmon-subscribe-scalar-loopback-probe` | `pktmon` probe; still did not expose `::1` NMX loopback traffic |
Converted pcap summaries:
```text
analysis\network\pcap-summary.txt
captures\011-pktmon-subscribe-scalar-loopback-probe\pcap-summary.txt
```
## Network capture status
`netsh trace` and `pktmon` both captured external/background traffic and can be
converted to pcapng. They did not expose the local loopback session that
`NmxSvc.exe` keeps open on IPv6 loopback.
Current `NmxSvc.exe` socket evidence during the run:
```text
TCP 10.100.0.48:5026 LISTEN
UDP 10.100.0.48:5026
TCP ::1:49829 <-> ::1:49704 ESTABLISHED
```
Interpretation: the payload needed for a native managed client is probably on
the local `::1` connection between the 32-bit MXAccess stack and `NmxSvc.exe`,
not on the physical NIC path. Capturing that requires one of:
- working Npcap loopback capture,
- API Monitor / debugger tracing at Winsock or COM method boundaries,
- ETW provider capture if the AVEVA NMX components emit enough payload detail,
- direct lower-level COM tracing around `INmx4.PutRequest2` / `GetResponse2` and
`IDataClient` methods.
Wireshark 4.6.4 and `etl2pcapng` 1.11.0 were installed.
Update after interactive install: Npcap 1.87 is now installed and working.
`dumpcap -D` lists `\Device\NPF_Loopback (Adapter for loopback traffic capture)`.
The verification capture is:
```text
captures\012-npcap-loopback-subscribe-scalar\
```
Files:
```text
loopback.pcapng
harness.log
nmx-loopback-frames.tsv
```
The focused loopback capture includes the active MXAccess/NMX conversation:
```text
::1:59335 <-> ::1:49704
803 frames, about 84 kB, duration 7.6793 s
```
It also saw the pre-existing service connection:
```text
::1:49829 <-> ::1:49704
4 frames, about 526 bytes
```
This confirms Npcap loopback capture is the correct mechanism for collecting
the actual local NMX payloads needed for protocol reconstruction.
## Npcap loopback protocol captures
The repeatable runner is:
```text
analysis\scripts\run_loopback_capture.ps1
```
Focused captures completed after Npcap verification:
| Folder | Result |
| --- | --- |
| `captures\013-loopback-subscribe-scalars` | good bool/int/string subscribe |
| `captures\014-loopback-subscribe-array-bracketed` | good string-array subscribe using `[]` suffix |
| `captures\015-loopback-subscribe-invalid` | invalid reference subscribe |
| `captures\016-loopback-write-test-int-advised` | advised same-value write succeeds |
Extraction and summary helpers:
```text
analysis\scripts\extract_nmx_loopback.py
analysis\scripts\extract_tcp_conversations.py
analysis\scripts\decode_tcp_payload_packets.py
analysis\scripts\decode_mixed_local_stream.py
analysis\scripts\analyze_write_window.py
analysis\scripts\diff_write_window_records.py
analysis\scripts\run_frida_mx_trace.ps1
analysis\scripts\extract_frida_trace.py
analysis\scripts\summarize_dcerpc.py
analysis\network\dcerpc-loopback-summary.tsv
analysis\network\write-window-tcp-payloads.tsv
```
Important packet result: the `::1:<ephemeral> <-> ::1:49704` traffic is DCE/RPC,
not a simple tag-string socket protocol. The observed interface UUIDs are:
```text
4e0c90df-e39d-4164-a421-ace89484c602
1981974b-6bf7-46cb-9640-0260bbb551ba
```
Those UUIDs were not found as direct keys under the checked COM registry
interface, CLSID, or TypeLib areas. The likely decode targets are therefore the
native proxy/stub binaries:
```text
C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvcps.dll
C:\Program Files (x86)\ArchestrA\Framework\Bin\WWProxyStub.dll
```
Good scalar subscribe, good array subscribe, and successful advised write share
the same main `49704` DCE/RPC shape: 165 ctx-1 opnum-3 request/response pairs,
10 ctx-0/ctx-1 setup groups, and 3 ctx-1 opnum-5 pairs. The invalid subscribe
adds one more setup group and 11 more opnum-3 pairs, matching the observed
behavior that invalid references fail during advise/resolution, not `AddItem`.
The successful write has a second important observation. The harness write call
occurred at relative time about `10.34s`, and the write-complete callback at
about `10.55s`. No `49704` DCE/RPC frames appear in that exact window. The
active payload is primarily on:
```text
127.0.0.1:57415 <-> 127.0.0.1:57433
```
That stream is compact binary traffic with small control messages and apparent
little-endian length prefixes. This means the managed replacement likely needs
to reproduce both the DCE/RPC coordination path and a separate local binary
callback/request channel.
The write-window stream is extracted as:
```text
captures\016-loopback-write-test-int-advised\tcp-stream-127_0_0_1_57415-to-127_0_0_1_57433.bin
captures\016-loopback-write-test-int-advised\tcp-stream-127_0_0_1_57433-to-127_0_0_1_57415.bin
```
The stream has a mixed-record framing:
- 12-byte control records: `int32 code_or_status`, `int32 token_low`,
`int32 token_high`.
- Data records: `uint32 body_length`, followed by the data body.
- Positive control values can announce one or more following data records.
- `-1` appears as a normal acknowledgement/status control.
- `-2` appears around write-window status/control exchanges.
Additional differential write captures:
| Folder | Result |
| --- | --- |
| `captures\017-loopback-write-test-int-100` | value changed `99 -> 100`; pcap usable |
| `captures\018-loopback-write-test-int-101` | write succeeded, but pcap is header-only and should not be used |
| `captures\019-loopback-write-test-int-101-rerun` | pcap usable, but same-value write because value was already `101` |
| `captures\020-loopback-write-test-int-102` | value changed `101 -> 102`; pcap usable |
| `captures\021-loopback-write-test-int-sequence-103-105` | same-session sequence changed `102 -> 103 -> 104 -> 105`; pcap usable |
| `captures\022-frida-write-test-int-sequence-106-108` | Frida hooks validated Ghidra RVAs; first buffer dump helper was incomplete |
| `captures\023-frida-write-test-int-sequence-109-111` | Frida hooks captured raw write values in `LmxProxy` and `NmxAdptr` buffers |
| `captures\024-frida-write-test-bool-sequence` | bool write matrix: `VT_BOOL`, 37-byte `PutRequest`, value slot at offset `18` |
| `captures\025-frida-write-test-float-sequence` | float write matrix: 40-byte `PutRequest`, `float32` at offset `18` |
| `captures\026-frida-write-test-double-sequence` | double write matrix: 44-byte `PutRequest`, `float64` at offset `18` |
| `captures\027-frida-write-test-string-sequence` | string write matrix: UTF-16LE payload at offset `26` |
| `captures\028-frida-write-test-datetime-sequence` | datetime write matrix: outbound UTF-16LE display string; callback FILETIME |
| `captures\029-frida-write-test-int-array` | int array write succeeds; packed `int32` values at `PutRequest` offset `28` |
| `captures\030-frida-write-test-bool-array` | bool array write succeeds, but alternating requested values returned as paired values; needs follow-up |
| `captures\031-frida-write-test-float-array` | float array write succeeds; packed `float32` values at `PutRequest` offset `28` |
| `captures\032-frida-write-test-double-array` | double array write succeeds; packed `float64` values at `PutRequest` offset `28` |
| `captures\033-frida-write-test-string-array` | string array write succeeds; per-element UTF-16LE records |
| `captures\034-frida-write-test-datetime-array` | datetime array write succeeds, but body dump was truncated by the old 256-byte Frida cap |
| `captures\035-frida-write-test-datetime-array-full` | datetime array rerun with full 4096-byte Frida dump cap |
| `captures\036-frida-write-secured-test-int` | `WriteSecured` against Operate int rejected with `0x80004021` before value-bearing body |
| `captures\037-frida-write-secured2-test-int` | `WriteSecured2` against Operate int rejected with `E_INVALIDARG` before value-bearing body |
| `captures\038-frida-write-secured-protectedvalue` | `WriteSecured` against real SecuredWrite bool rejected with `0x80004021` |
| `captures\039-frida-write-secured-verified-protectedvalue1` | `WriteSecured` against real VerifiedWrite bool rejected with `0x80004021` |
| `captures\040-frida-write-normal-secured-protectedvalue` | normal `Write` with user/security `2` succeeds against SecuredWrite bool |
| `captures\041-frida-write-normal-verified-protectedvalue1` | normal `Write` with user/security `3` succeeds against VerifiedWrite bool |
| `captures\042-frida-write2-test-int-timestamp` | `Write2` succeeds; int at offset `18`, FILETIME at offset `24` |
| `captures\043-frida-loopback-write-test-int-115` | combined Frida plus Npcap loopback capture; exact adapter bodies are not present verbatim in TCP streams |
| `captures\044-frida-loopback-write-test-int-123456789` | combined Frida plus Npcap loopback capture with distinctive value; raw scalar also absent from full pcap payload scan |
The same-session sequence was captured with:
```text
--scenario=write --tag=TestChildObject.TestInt --type=int --values=103,104,105 --user-id=1 --write-delay-ms=1000 --write-interval-ms=700 --duration=5
```
Generated write-window analyses:
```text
captures\017-loopback-write-test-int-100\write-window-mixed-records.tsv
captures\020-loopback-write-test-int-102\write-window-mixed-records.tsv
captures\021-loopback-write-test-int-sequence-103-105\write-window-mixed-records.tsv
analysis\network\write-window-body-diff-017-vs-020.tsv
analysis\network\write-window-body-diff-021-w0-vs-w1.tsv
analysis\network\write-window-body-diff-021-w1-vs-w2.tsv
```
The sequence capture is important because it keeps one MXAccess session alive
while changing only the requested int value. The decoded local-stream records
around write-complete still do not contain `103`, `104`, or `105` as plain
little-endian int32 payloads. The bytes that move consistently in the visible
records are request/session counters and checksummed or opaque body fields.
The pcap-only mixed-stream layer did not isolate the scalar value. Headless
Ghidra plus Frida then located it one native layer higher:
```text
CLMXProxyServer::Write variant A RVA 0x12c0c
CNmxAdapter::PutRequest RVA 0x15169
CNmxAdapter::TransferData RVA 0x10996
CNmxAdapter::ProcessDataReceived RVA 0x112da
```
In `captures\023-frida-write-test-int-sequence-109-111`, the raw scalar values
are visible as little-endian int32:
| Function | Body size | Value offsets |
| --- | ---: | --- |
| `CLMXProxyServer::Write` | call args | `args[5] = 109, 110, 111` |
| `CNmxAdapter::PutRequest` | `40` | offset `18` |
| `CNmxAdapter::TransferData` | `86` | offset `64` |
| `CNmxAdapter::ProcessDataReceived` | `88` | offset `84` |
The later Frida captures generalize the body format:
| Type | `PutRequest` | `TransferData` | Callback/update | Encoding |
| --- | --- | --- | --- | --- |
| bool | size `37`, offset `18` | size `83`, offset `64` | size `85`, 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`, offset `18` | size `86`, offset `64` | size `88`, offset `84` | little-endian `int32` |
| float | size `40`, offset `18` | size `86`, offset `64` | size `88`, offset `84` | little-endian `float32` |
| double | size `44`, offset `18` | size `90`, offset `64` | size `92`, offset `84` | little-endian `float64` |
| string | size `58` or `60`, offset `26` | size `104` or `106`, offset `72` | size `106` or `108`, offset `92` | UTF-16LE |
| datetime | size `86`, offset `26` | size `132`, offset `72` | size `98`, offset `88` | outbound UTF-16LE display string; callback FILETIME |
Full matrix:
```text
analysis\frida\write-body-matrix.tsv
```
Array matrix:
```text
analysis\frida\write-array-body-matrix.tsv
```
Write-mode matrix:
```text
analysis\frida\write-mode-matrix.tsv
```
Frida-to-TCP mapper:
```text
analysis\scripts\run_frida_loopback_capture.ps1
analysis\scripts\map_frida_to_tcp.py
analysis\scripts\parse_dcerpc_streams.py
captures\043-frida-loopback-write-test-int-115\frida-to-tcp-map.tsv
captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv
```
Array body summary:
| Type | `PutRequest` | `TransferData` | Callback/update | Encoding |
| --- | --- | --- | --- | --- |
| int[] | size `86`, first value offset `28` | size `132`, first value offset `74` | size `134`, first value offset `94` | descriptor kind `0x42`, count `10`, width `4`, packed `int32` |
| bool[] | size `66`, first value offset `28` | size `112`, first value offset `74` | size `114`, first value offset `94` | descriptor kind `0x41`, count `10`, width `2`; observed value pairing needs follow-up |
| float[] | size `86`, first value offset `28` | size `132`, first value offset `74` | size `134`, first value offset `94` | descriptor kind `0x43`, count `10`, width `4`, packed `float32` |
| double[] | size `126`, first value offset `28` | size `172`, first value offset `74` | size `174`, first value offset `94` | descriptor kind `0x44`, count `10`, width `8`, packed `float64` |
| string[] | size `256`, first string bytes at `41` | size `302`, first string bytes at `87` | size `304`, first string bytes at `107` | descriptor kind `0x45`, per-element UTF-16LE variable records |
| datetime[] | size `596`, first string bytes at `41` | size `642`, first string bytes at `87` | size `214`, first FILETIME at `94` | outbound per-element display strings; callback/update FILETIME sequence |
Secured/verified write result: the public `WriteSecured` and `WriteSecured2`
methods are exposed by COM but did not produce value-bearing requests in these
captures. Actual `SecuredWrite` and `VerifiedWrite` attributes accepted normal
`Write` calls when the fourth argument matched the Galaxy security
classification (`2` or `3`).
`Write2` result: the timestamped int write still uses a 40-byte body with the
value at offset `18`. A FILETIME timestamp is embedded at `PutRequest` offset
`24`, `TransferData` offset `70`, and callback/update offset `75`.
Transport correlation result: in capture `043`, the raw `int32` value `115`
appears in TCP streams, but the exact Frida `PutRequest`, `TransferData`, and
callback bodies do not. The `::1:49704` hits around the scalar align with
DCE/RPC metadata/call IDs rather than the native adapter body.
Capture `044` uses `123456789` to avoid that ambiguity. The raw scalar is not
found in a full pcap payload scan, in parsed `::1:49704` DCE/RPC stubs, or in
the mixed local stream. This confirms the wire representation is transformed or
encoded before TCP.
Detailed notes are in:
```text
docs\Loopback-Protocol-Findings.md
docs\NMX-COM-Contracts.md
```
## Protocol facts established
- `Register` returns session handle `1` for these short-lived runs.
- `AddItem` allocates local item handles but does not prove the reference exists.
- `AdviseSupervisory` triggers item resolution and initial data/status callback.
- Good scalar reads return:
- `MXSTATUS_PROXY.success = -1`
- `category = MxCategoryOk`
- `detectedBy = MxSourceRequestingLmx`
- `detail = 0`
- quality `192`
- Invalid subscribe returns:
- value `null`
- quality `0`
- `category = MxCategoryConfigurationError`
- `detectedBy = MxSourceRequestingLmx`
- `detail = 6`
- Array name missing `[]` returns:
- value `null`
- quality `0`
- `category = MxCategoryConfigurationError`
- `detectedBy = MxSourceRespondingAutomationObject`
- `detail = 1003`
- Successful write completion returns:
- `success = -1`
- `category = MxCategoryOk`
- `detectedBy = MxSourceRespondingAutomationObject`
- `detail = 0`
Later targeted non-core type captures:
- `062-frida-subscribe-intl-shortdesc`: `TestChildObject.ShortDesc`
(`InternationalizedString`) resolves and advises. A callback record used normal
string wire kind `0x05` with compact empty payload `04 00 00 00`.
- `063-frida-subscribe-elapsed-time-deadband`:
`TestMachine_001.TestAlarm001.Alarm.TimeDeadband` (`ElapsedTime`) resolves and
advises. A callback record used wire kind `0x07` with four-byte zero payload.
- `064-frida-subscribe-intl-percent` and `065-frida-subscribe-intl-mb`:
non-empty internationalized-string references resolved and advised but did not
emit value callbacks during the capture window.
- `066` through `069`: timestamped `Write2` captures for bool, float, double,
and string. Float, double, and string use the same fixed/variable timestamped
suffix shape as int. Timestamped bool uses wire kind `0x01`, one value byte,
then the normal timestamp suffix.
- `070` through `072`: timestamped `Write2` captures for int[], bool[], and
string[]. These match the existing managed array timestamp encoder. The
bool-array capture preserves the earlier MXAccess marshaling behavior where
requested alternating bools arrive as paired true/false values.
- `073`, `074`, and `076`: timestamped `Write2` captures for float[], double[],
and datetime[]. These match the managed array timestamp encoder. The initial
`075` datetime[] run used the wrong tag name and did not emit the expected
`0x37` write body; `076` is the valid capture for `TestDateTimeArray[]`.
- `mxaccess-suspend-*` / `mxaccess-activate-*`: `Suspend` and `Activate` throw
`0x80070057` before `AdviseSupervisory`. The advised variants succeed on
scan-state targets: suspend returns pending/requesting-LMX, activate returns
ok/requesting-LMX. Frida captures `077` and `078` show no additional NMX
request body after the public method call, so this behavior appears local to
the MXAccess/LMX layer once the item has been advised.
- `079-frida-add-buffered-advise-testint`: `AddBufferedItem("TestInt",
"TestChildObject")` followed by `AdviseSupervisory` sends an item-control
`0x10` reference-registration body for `TestInt.property(buffer)` in context
`TestChildObject`. It does not send a normal `0x1f` advise body for the
buffered handle.
- `080-frida-buffered-external-write-testint`: while the buffered handle is
advised, adding normal writer handles in the same session sends normal
reference-registration bodies, but no `OnBufferedDataChange` payload was
observed. This capture supplied the stable normal and buffered `0x10`
registration bodies, plus the matching `0x11` registration-result frames,
used by `NmxReferenceRegistrationMessage` and
`NmxReferenceRegistrationResultMessage` tests.
- `085-frida-subscribe-property-buffer` and
`086-frida-write-property-buffer`: direct literal
`TestChildObject.TestInt.property(buffer)` add/advise follows normal
`AdviseSupervisory`, not `AddBufferedItem`; the item-control `0x10`
registration uses the full literal item with an empty context and receives a
`0x11` registration result containing the same Base Runtime Object internal
error text. No `OnBufferedDataChange` event was fired.
- `087-frida-authenticate-administrator-empty` and
`088-frida-authenticate-invalid-empty`: password-redacted auth hook shows
`CLMXProxyServer.AuthenticateUser` returning `S_OK` and user ID `1` for both
`Administrator` and an invalid user name with password length `0`. The only
NMX traffic observed after the auth call is the normal unregister-time system
reference cleanup, not an auth request body.
- `111-frida-write-secured-auth-protectedvalue` and
`112-frida-write-secured-auth-verified-protectedvalue1`: pre-authenticating
with `AuthenticateUser` returns user handle `1`, but public `WriteSecured`
still returns `0x80004021` before any value-bearing NMX write body is emitted.
- `113-frida-write-secured2-auth-protectedvalue`,
`114-frida-write-secured2-auth-protectedvalue-false`,
`115-frida-write-secured2-auth-protectedvalue-true`, and
`116-frida-write-secured2-auth-verified-protectedvalue1`: authenticated
public `WriteSecured2` succeeds and emits NMX command `0x38` over normal
transfer kind `Write` (`3`). The decoded boolean body layout is: command and
14-byte reference-handle projection, boolean wire kind `0x01`, a four-byte
boolean scalar whose first byte carries the value, FILETIME timestamp, a
16-byte current-user authenticator token, UTF-16LE client-name byte length,
null-terminated UTF-16LE client name, a 16-byte verifier token, `0xffff`,
client token, and write index. The verifier token is all zeros when
`VerifierUserId` is `0` and equals the authenticated token when the verifier
handle is `1`.
- `117-frida-write-secured2-auth-testint`: after pre-authentication, public
`WriteSecured2` also succeeds against `TestChildObject.TestInt` and emits
command `0x38` with integer wire kind `0x02`. This proves the secured2 body
is not boolean-only: it reuses the normal timestamped `0x37` value and
timestamp prefix, then appends current-user token, client name, verifier
token, `0xffff`, client token, and write index.
- `089-frida-write-testint-wrong-type`: writing string `not_an_int` to integer
`TestChildObject.TestInt` sends a normal `0x37` write body using string wire
kind `0x05`. NMX responds with a length-prefixed completion-only status body
whose inner completion byte is `0x41`; MXAccess returns `S_OK` from `Write`
but does not fire `OnWriteComplete` during the harness wait.
- `090-frida-write-invalid-reference`: invalid reference add/advise produces
the expected `0x10` registration and `0x11` registration-result failure. The
later public `Write` call returns `S_OK` but no value-bearing `0x37` body or
write-complete event is observed.
- `091-frida-write-testint-double-type`,
`092-frida-write-testbool-string-type`, and
`093-frida-write-testdatetime-string-type`: double-to-int writes emit
a length-prefixed completion-only byte `0x00`, but MXAccess still does not
fire `OnWriteComplete`; string-to-bool and string-to-time wrong-type writes
match the string-to-int failure pattern and emit completion byte `0x41`.
- `analysis/ghidra/exports/LmxProxy.dll.buffered-decompile.md`: decompile of
`AddBufferedItem`, `Fire_OnBufferedDataChange`, and
`SetBufferedUpdateInterval`. It confirms the `.property(buffer)` suffix,
the buffered item-record marker, the seven-argument event firing path, and
100 ms tick rounding for buffered update intervals.
- `analysis/ghidra/exports/LmxProxy.dll.buffered-event-xrefs.md` and
`analysis/ghidra/exports/LmxProxy.dll.buffered-event-caller-decompile.md`:
`Fire_OnBufferedDataChange` has one direct caller, `FUN_1001657f`. That
function is the same native `OnDataChange callback received` path used for
normal data changes. It looks up the item record and branches on a buffered
item flag at offset `0x28`: normal items call the `_ILMXProxyServerEvents`
`OnDataChange` helper, while buffered items convert the value to value,
quality, and timestamp SAFEARRAY variants and call
`_ILMXProxyServerEvents2::Fire_OnBufferedDataChange`.
- `analysis/ghidra/exports/LmxProxy.dll.buffered-value-conversion-decompile.md`:
decompile of the buffered value conversion helper shows the public buffered
event value argument is a SAFEARRAY of values, the quality argument is a
`VT_I2` SAFEARRAY, the timestamp argument is a `VT_UI8`/FILETIME SAFEARRAY,
and the status argument is the normal `MXSTATUS_PROXY[]` SAFEARRAY. This
confirms the event shape even though a live buffered payload has not yet
been produced.
- `analysis/ghidra/exports/LmxProxy.dll.auth-decompile.md`: decompile of
`AuthenticateUser` and `ArchestrAUserToId`. Both increment a session-local
user counter and store a mapping from that generated handle to a GUID/token
identity before returning the generated handle to the public API caller.
- `analysis/ghidra/exports/LmxProxy.dll.events-decompile.md`: decompile of
`Fire_OnWriteComplete` and `Fire_OperationComplete`. Both build the same
three-argument COM event payload: server handle, item handle, and one
`MXSTATUS_PROXY` SAFEARRAY. `OnWriteComplete` dispatches event ID `2`;
`OperationComplete` dispatches event ID `3`. No capture in this set emitted
`mx.event.operation-complete`.
- `analysis/ghidra/exports/LmxProxy.dll.event-xrefs.md` and
`analysis/ghidra/exports/LmxProxy.dll.event-callers-decompile.md`: generated
with headless Ghidra to identify the event helper callers. `Fire_OnWriteComplete`
is called only from `FUN_10016b50`, which logs
`OnSetAttributeResult callback received`. `Fire_OperationComplete` is called
only from `FUN_10016d4b`, which logs `OperationComplete callback received`.
This confirms that mapping write completion statuses to both public events
would be incorrect; `OperationComplete` needs a distinct native callback
capture before the managed compatibility event can fire.
- `analysis/ghidra/exports/LmxProxy.dll.operation-candidates-decompile.md`:
decompile of public operation candidates. `RemoveItem` performs local item
cleanup, while `Suspend` and `Activate` query an `IMxScanOnDemand` interface
and synchronously call vtable offsets `0x0c` and `0x10`, respectively. These
paths did not reveal a call to `Fire_OperationComplete`, matching captures
`077` and `078`, which returned status structs but emitted no operation event.
- `118-frida-suspend-advised-scanstate-long` and
`119-frida-activate-advised-scanstate-long`: reran advised
`DevAppEngine.ScanState` suspend/activate with direct hooks on
`CUserConnectionCallback.OnSetAttributeResult` and
`CUserConnectionCallback.OperationComplete`. The hooks installed, but neither
callback entry point was called during the 20 second waits. This strengthens
the conclusion that these public scan-on-demand calls return local status and
do not trigger the public `OperationComplete` event on this node.
- `094-frida-buffered-separate-writer`: the harness was adjusted so
`buffered-external-write` registers a separate writer server handle before
adding/advising/writing `TestChildObject.TestInt`. The capture still produced
no `mx.event.buffered-data-change` and no `Fire_OnBufferedDataChange` Frida
entry. It did show the buffered `0x10` registration/`0x11` result for
`TestInt.property(buffer)` in context `TestChildObject`, plus normal writer
subscription/data callbacks. This rules out same-server-handle writer reuse
as the reason buffered callbacks were absent.
- `120-frida-buffered-history-testhistoryvalue`: first historized-attribute
buffered attempt against `TestHistoryValue`, but the harness argument used
`--item-context` instead of its actual `--context` switch, so MXAccess
registered `TestHistoryValue.property(buffer)` with an empty context. This is
retained only as a harness-option correction.
- `121-frida-buffered-history-testhistoryvalue-context`: repeated the capture
with `--context=TestMachine_001`. GR identifies
`TestMachine_001.TestHistoryValue` as a deployed, historized integer dynamic
attribute. Native MXAccess emitted the expected buffered `0x10`
registration and `0x11` result for
`TestHistoryValue.property(buffer)` in context `TestMachine_001`, and the
separate writer session successfully wrote `201`, `202`, and `203` through
`TestMachine_001.TestHistoryValue`. No public `mx.event.buffered-data-change`
and no `Fire_OnBufferedDataChange` Frida entry were observed.
- `122-frida-buffered-history-testhistoryvalue-plainadvise`: added a harness
`--plain-advise` probe switch and repeated the same historized buffered
scenario using public `Advise` instead of `AdviseSupervisory`. The
registration/result bodies matched the context-bearing buffered shape, writer
writes succeeded, and writer-session normal `0x32` data callbacks were seen,
but the buffered subscriber still did not enter `Fire_OnBufferedDataChange`.
This makes the remaining buffered gap a runtime/source-delivery condition,
not a plain-versus-supervisory advise mismatch.
- `095-frida-write-elapsed-int`: writing `1000` as an `Int32` to
`TestMachine_001.TestAlarm001.Alarm.TimeDeadband` emitted a normal `0x37`
write body with integer wire kind `0x02`; MXAccess did not emit a special
elapsed write kind for an integer caller value.
- `096-frida-write-intl-string`: writing `"hello-native"` as a `string` to
`TestChildObject.ShortDesc` emitted a normal `0x37` write body with string
wire kind `0x05`. The runtime returned a completion-only status byte `0xef`,
so success semantics still need more captures, but outbound encoding is now
defined for the caller-variant path.
- `097-frida-write-bool-array-pattern`: first attempted a bool-array pattern
with `--values`, which the harness interprets as ten separate writes of
one-element arrays. This capture is useful only as a harness argument
reminder.
- `098-frida-write-bool-array-pattern-10`: writing one ten-element
`bool[]` value with the requested pattern
`true,false,false,true,true,false,true,false,false,true` confirmed the x86
MXAccess COM automation path still emits a paired/shifted
VARIANT_BOOL-style wire payload:
`true,true,false,false,false,false,true,true,true,true`. The NMX array
descriptor remains `0x41`, count `10`, width `2`. The managed encoder keeps
direct per-element `bool[]` encoding for native .NET callers, while the
observed x86 COM projection is preserved as a golden compatibility capture.
- `099-frida-plain-advise-testint`: public `CLMXProxyServer.Advise` for
`TestChildObject.TestInt` emitted the same item-control `0x1f` body shape as
the earlier `AdviseSupervisory` scalar subscription capture, including the
trailing option value `3`. The wrapper's shared `Advise`/`AdviseSupervisory`
path now has capture support for this scalar case.
- `100-frida-subscribe-string-array`: subscribing to
`TestChildObject.TestStringArray[]` produced a subscription-status callback
with wire kind `0x45`, count `10`, width code `4`, and string-array element
records, but the observed callback buffer stopped inside the final `"JJ10"`
element after `"JJ1"`. MXAccess did not raise a public
`OnDataChange` event.
- `101-frida-write-string-array-update`: writing
`KA1;KB2;KC3;KD4;KE5;KF6;KG7;KH8;KI9;KJ10` emitted a complete outbound
`0x37` string-array body. The following callback again carried a `0x45`
string-array record but stopped inside the final `"KJ10"` element after
`"KJ1"`, and no public data-change event was observed. Current managed
decoding treats this malformed callback value as incomplete rather than
fabricating a string array.
- `102-frida-subscribe-intl-shortdesc-after-write`: subscribing to
`TestChildObject.ShortDesc` after the earlier string write still produced the
compact empty string callback form (`wire kind 0x05`, record length `4`) and
no public data-change event. The earlier `096` write returned completion-only
`0xef`, so it did not establish a non-empty `InternationalizedString`
callback value.
- `103-frida-subscribe-elapsed-after-write`: subscribing to
`TestMachine_001.TestAlarm001.Alarm.TimeDeadband` produced a non-zero
`ElapsedTime` callback value using wire kind `0x07` followed by a 4-byte
little-endian millisecond count (`00 e4 0b 54`, `0x540be400`). This confirms
the existing elapsed-time callback decoder works for non-zero values as well
as the earlier zero capture.
## Managed x64 live probes
- `probe-remqi-managed`: a .NET 10 x64 process using managed NTLM activation
successfully resolved `INmxService2`, completed `RemQueryInterface`, and
returned partner version `6`.
- `probe-session-write`: direct managed `WriteAsync` to
`TestChildObject.TestInt` returns success through `INmxService2.TransferData`.
The managed sender now uses the captured transfer kind for normal writes
(`Write`, value `3`) rather than the earlier item-control kind.
- `probe-session-subscribe`: fixed local engine IDs caused stale registration
collisions and `UnAdvise` failures (`0x80041101`). The managed defaults now
derive the local engine ID from the current process ID; unique engine IDs
receive both `0x32` subscription-status and `0x33` data-update callbacks and
clean up without that failure.
- The live managed subscriber receives status-only scalar callbacks for
`TestChildObject.TestInt`: the raw `0x33` body contains status, quality,
timestamp, and wire kind `0x02`, but no four-byte integer payload. The same
status-only result occurs when an x86 MXAccess writer changes the tag while
the managed subscriber is active. This narrows the missing value-delivery
piece to the pre-advise AddItem/metadata registration path that MXAccess
emits as `0x17`, not to the scalar callback decoder.
- A managed encoder for the observed `0x17` metadata body reproduces the
capture exactly and `NmxSvc` accepts it before advise, but it still does not
turn the managed scalar callback into a value-bearing record. The `0x17`
body is therefore documented as a captured metadata primitive, not a complete
AddItem/value-subscription registration.
- Replaying the observed pre-advise `0x17` metadata body from the managed
NTLM/DCOM path now reproduces the MXAccess-like callback sequence in x64:
a 706-byte `0x40` metadata response containing
`DevPlatform.GR.TimeOfLastDeploy`,
`DevPlatform.GR.TimeOfLastConfigChange`, and the text
`An internal error occurred in the Base Runtime Object`; a 151-byte `0x32`
metadata status callback with two datetime records; the 92-byte operation
status frame; then the normal 108-byte scalar `0x32` status-only callback
for `TestChildObject.TestInt`. This proves the pre-advise metadata primitive
is live and decoded enough to inspect, but its runtime result is an internal
base-object error rather than successful value-subscription state.
- A same-session managed subscribe-then-write probe also remains status-only:
after `SubscribeAsync(TestChildObject.TestInt)`, managed `WriteAsync(331)`
returns success, then the callback is `0x33` with wire kind `0x02` but no
integer payload. This rules out cross-client delivery as the only cause; the
native library still needs additional LMX item/write state beyond the current
GR handle, NMX advise, and direct NMX write bodies.
- Ghidra review of `CReferenceStringResolutionService` explains why replaying
only the wire-visible metadata request is insufficient. The resolver compares
`TimeOfLastDeploy` and `TimeOfLastConfigChange` against pending reference
status details, and on a usable GR subscription status it calls an
in-process `OnSetAttributeResult` path directly before removing the pending
request. That state mutation is not produced by the current managed x64
replay, which only sends the `NmxSvc` request and receives callbacks.
- Fixed Frida captures `104` and `105` corrected the stack argument mapping for
`PrebindReference` and `UserRegisterPreboundReference`. Native
`TestChildObject.ShortDesc` returns public prebound/reference handles of `1`,
sends `0x1f` advise with transfer kind `ItemControl` (`2`), and its
`IMxReference.GetMxHandle` value uses property id `10` even though the GR
attribute category is `11`.
- After changing the managed sender to use transfer kind `2` for `0x1f` advise
and changing the GR resolver to synthesize value handles with property id
`10`, .NET 10 x64 `SubscribeAsync(TestChildObject.ShortDesc)` receives the
native-equivalent 112-byte callback: command `0x32`, status/detail `3/0`,
quality `0x00C0`, wire kind `0x05`, and empty string value. This proves the
primary value-subscription path can be reproduced in full managed code for at
least one x86 value-bearing capture.
- A managed-client regression check now asserts the distinct transfer kinds:
normal `0x37` writes use transfer kind `Write` (`3`), while `0x1f` advise
uses transfer kind `ItemControl` (`2`). The generated `ShortDesc`
`"hello-native"` write body matches capture `096` byte-for-byte, including
the value handle with property id `10`.
- Live x64 subscribe-write probes after that correction matched the native
outcomes: `TestChildObject.ShortDesc` still returns completion-only byte
`0xef`, which is therefore native-equivalent for the current
`InternationalizedString` caller path rather than a managed-only transport
failure; `TestChildObject.TestInt` returns completion-only byte `0x00` and a
status-only `0x33` update callback with wire kind `0x02`.
- Fresh x86 MXAccess baseline captures on the current VM state:
`106-native-subscribe-testint-current` and
`107-native-write-testint-current` show public MXAccess also raises no
`mx.event.data-change` for `TestChildObject.TestInt` subscribe-only or
subscribe-then-write, even though the write call returns success. Older
captures `003`, `011`, `012`, `017`, and `018` prove this tag previously
emitted value-bearing public events, so the current status-only managed
result is not by itself a managed transport failure; it reflects the current
runtime/Galaxy state or deployment value-delivery state.
- Fresh current-state capture `108-native-subscribe-scalar-current` subscribed
to `TestBool`, `TestInt`, `TestFloat`, `TestString`, and `ShortDesc` through
native x86 MXAccess and raised no public `mx.event.data-change` for any of
them. Because native x86 also suppresses the decoded empty `ShortDesc`
adapter callback at the public event layer, `MxNativeCompatibilityServer`
now suppresses empty `InternationalizedString` `DataChanged` promotion while
leaving the low-level `MxNativeSession.CallbackReceived` event intact.
- The new .NET 10 x64 compatibility probe
`--probe-compatibility-subscribe` validates that public-facade behavior:
`TestChildObject.ShortDesc` and `TestChildObject.TestInt` both reported
`compat_data_changes=0`, matching fresh native x86 public-event behavior on
the current VM state.
- The companion `--probe-compatibility-subscribe-write` probe validates public
write-path facade behavior: `TestChildObject.TestInt = 793` and
`TestChildObject.ShortDesc = hello-compat` both completed the managed write
call but reported `compat_subscribe_write_data_changes=0` and
`compat_subscribe_write_completes=0`, matching the current native public
behavior where completion-only NMX statuses do not surface as
`OnWriteComplete`.
- Invalid-reference parity is now modeled in the compatibility facade:
`NoSuchObject_999.NoSuchAttr` returns an item handle from `AddItem`, and
`AdviseSupervisory` emits one public data-change with `value=null`,
`quality=0`, `status_success=0`, `ConfigurationError`, `RequestingLmx`, and
detail `6`, matching captures `007` and `015`. A subsequent compatibility
write to that invalid handle returns without adding a public write-complete
event, matching capture `090`.
- A mixed multi-item .NET 10 x64 compatibility probe with
`ShortDesc`, `TestInt`, and `NoSuchObject_999.NoSuchAttr` validated handle
routing in one server session: items `1` and `2` produced zero public changes
on the current VM state, while item `3` produced exactly one
configuration-error data-change.
- Compatibility write argument validation now mirrors two x86 error captures:
normal `Write` after `AddItem` but before advise returns `0x80070057`, as in
captures `008`/`009`, and normal `Write` against an `AddBufferedItem` handle
returns `0x80070057`, as in
`mxaccess-add-buffered-write-testint-context.log`.
- `captures\109-native-post-remove-errors` records stale item-handle parity
after `RemoveItem`. Native x86 MXAccess returns `ArgumentException`
`0x80070057` for `Advise`, `AdviseSupervisory`, `UnAdvise`, `Write`,
`Write2`, `Suspend`, `Activate`, and a second `RemoveItem` against the
removed handle. The .NET 10 x64 `--probe-compatibility-post-remove` probe now
reports the same `0x80070057` result for all of those operations.
- `captures\110-native-invalid-handle-errors` records invalid server-handle and
cross-server item-handle parity. Native x86 MXAccess returns
`ArgumentException` `0x80070057` for `AddItem`, `AddItem2`, `RemoveItem`,
`Advise`, `AdviseSupervisory`, `UnAdvise`, `Write`, `Write2`, `Suspend`,
`Activate`, and `Unregister` when the server handle is invalid, and also for
`RemoveItem`, `Advise`, and `Write` when the item handle belongs to another
server. The .NET 10 x64 `--probe-compatibility-invalid-handles` probe now
matches those HRESULTs.
- Literal property-reference parity is now covered for the observed buffer
property. Captures `085`/`086` show
`TestChildObject.TestInt.property(buffer)` resolving to base `TestInt` with
property id `0x32` and native handle
`01 00 01 00 02 00 05 00 36 d7 02 00 9b 00 32 00 3e da 00 00`.
`GalaxyRepositoryTagResolver` now recognizes that suffix, and the .NET 10
x64 compatibility subscribe/write probe for the literal reference reports
zero public data-changes and zero public write-completes, matching the native
public behavior in those captures.
- `AddItem2` context resolution has been broadened in the compatibility layer.
The live .NET 10 x64 probe now covers the captured simple context form
`TestInt` + `TestChildObject`, a dotted primitive attribute
`Alarm.TimeDeadband` + `TestMachine_001.TestAlarm001`, and a context-relative
property reference `TestInt.property(buffer)` + `TestChildObject`. All three
add and advise successfully; no public data-change is promoted on the current
VM state.
- A mixed .NET 10 x64 compatibility write probe now validates per-item routing
for `TestChildObject.TestInt`, `TestChildObject.ShortDesc`,
`TestChildObject.TestInt.property(buffer)`, and
`NoSuchObject_999.NoSuchAttr` in one server session. The three valid/literal
items report `data_changes=0` and `write_completes=0` on the current VM
state, while the invalid reference reports exactly one configuration-error
data-change and no write-complete. This confirms the compatibility facade does
not misattribute suppressed completion-only writes or invalid-reference
callbacks across item handles.
- A first post-fix scalar matrix shows more decoder/runtime work remains:
`TestChildObject.TestString` receives a `0x05` string record but the current
body ends before the declared string payload is complete, so the decoder
reports `value=null`; `TestChildObject.TestFloat` receives wire kind `0x03`
without enough bytes for a float payload; `TestChildObject.TestBool` receives
a 105-byte frame that stops inside the timestamp/kind area and is surfaced as
`UnparsedCallbackReceived`. These look like status/incomplete initial-value
callbacks on this node rather than handle-generation failures, because
`ShortDesc` now matches the native value-bearing path.
- Managed recovery lifecycle probe:
`MxNativeClient.Probe --probe-session-recover --recover-attempts=2
--recover-delay-ms=100 --tag=TestChildObject.TestInt --value=323` ran from
.NET 10 x64. It subscribed successfully, reported one recovery-started event,
zero failed-attempt events, one recovery-completed event, preserved one active
subscription after recovery, and wrote through the recovered session.
- Recovery callback policy is now explicit in the managed API: callbacks are
passed through during reconnect/replay and marked with `IsDuringRecovery`
instead of being suppressed or buffered. More live evidence is still needed to
catch actual replay-window callback delivery on larger subscription sets.
- Managed multi-subscription recovery probe:
`MxNativeClient.Probe --probe-session-recover-multi --recover-attempts=2
--recover-delay-ms=100` subscribed to `TestChildObject.TestInt`,
`TestChildObject.ShortDesc`, `TestMachine_001.ProtectedValue`, and
`TestMachine_001.ProtectedValue1`. Recovery replay preserved all four
subscriptions. The run observed two data callbacks and four unparsed
callbacks overall, but zero data, operation-status, reference-registration,
or unparsed callbacks with `IsDuringRecovery=true`.
- Managed multi-subscription recovery churn probe:
`MxNativeClient.Probe --probe-session-recover-multi --recover-attempts=2
--recover-delay-ms=100 --recover-concurrent-writes
--recover-write-start=330 --recover-write-count=5
--recover-write-delay-ms=10` used a separate writer session to write
`TestChildObject.TestInt` values `330`-`334` during recovery. Two writes
landed before the recovery-completed event. The recovering session preserved
all four subscriptions, observed four data callbacks overall, and marked two
data callbacks with `IsDuringRecovery=true`; operation-status,
reference-registration, and unparsed recovery-window counts remained zero.
- OperationComplete trigger analysis update: decompiled interop exposes
`IMxCallback2.OperationComplete(int lCallbackId, ref MxStatus, string)` plus
the `IDataConsumer.ActivateSuspend` / `ProcessActivateSuspend2` path returning
`ItemActiveResponse`. Public `LMXProxyServerClass.Suspend` and `Activate`
captures `118`/`119` used direct `IMxScanOnDemand` calls and did not enter the
callback. The next useful native trigger search should target DataConsumer
activate/suspend completion, not another plain public suspend/activate run.
- `aaMxDataConsumer.dll` import/probe: registry CLSID
`{85209FB2-0BA1-4594-BBC4-59D3DDAB823D}` maps to `MxDataConsumer Class` in
`C:\Program Files (x86)\Common Files\ArchestrA\Services\aaMxDataConsumer.dll`.
`tlbimp` generated `analysis\interop\Interop.aaMxDataConsumer.dll`, and
`ilspycmd` decompiled it into
`analysis\decompiled-interop\Interop.aaMxDataConsumer`. The new
`MxDataConsumerProbe` x86 harness can instantiate `DataConsumerClass`, call
`RegisterCallback`, resolve namespace strings to ID `1`, and call
`ResolveReference`, `subscribe`, `ActivateSuspend`, and
`ProcessActivateSuspend2`. Tested namespace strings remain disconnected
(`IsConnected=0`), no registration or subscription records are returned, and
`ProcessActivateSuspend2` returns `0x8007139F` (`ERROR_INVALID_STATE`). This
confirms the COM surface is callable but still missing the bootstrap needed to
attach it to the live namespace.
- `MxDataConsumerProbe --probe-dataclient` attempted to create
`DataClientClass` from the same imported type library. Creation failed with
`0x80040154` (`REGDB_E_CLASSNOTREG`) because CLSID
`{73BC4121-FF89-4762-901C-206E2BD9FE87}` is not registered on this node. The
ASB deployment config shows `ServiceHost1` at `net.tcp://localhost:4000/` and
`Default_ZB_MxDataProvider` publishing `IASBIData`, `IASBIDataV2`, and
`IDataV3`, but a registered client/factory is still needed before endpoint
connection probes can run.
- Headless Ghidra plus ILSpy on
`C:\Program Files (x86)\Common Files\ArchestrA\Services\aaMxDataConsumer.dll`
shows that its DataClient side is a mixed-mode wrapper around managed ASB
proxies. `CDataClientCLI.CreateConnection` sets the namespace string on a
`DataClientProxy` and starts the auto-connect worker; `DataClientProxy`
calls `CIDataVersionAdapterFactory.GetIDataAdapter(accessName)`, which calls
`IDataProxySelector.SelectProxyForLatestEndpoint(accessName, new
AsbMxDataSettings(), out error)`.
- `ASBIDataV2Adapter.dll` from the GAC contains `IDataProxySelector`. It first
checks `ASBDataV2Proxy.FindIDataEndpoint(accessName, DiscoveryScope.Global)`;
if any endpoints are discovered it returns `ASBDataV2Proxy`, otherwise it
falls back to `ASBDataProxy` V1. `ASBDataV2Proxy` searches LDS using scope
`domainname/<accessName>/global`.
- New x64 `AsbProxyProbe` results: access name `ZB` discovers one
`IASBIDataV2` endpoint,
`net.tcp://desktop-6jl3kko/ASBService/Default_ZB_MxDataProvider/IDataV2`,
with listen URIs on the host name and local IPs. Access names
`Default_ZB_MxDataProvider`, `Galaxy`, `localhost`, and `ZB2` discover no
IDataV2/IData endpoints. `AsbProxyProbe --access=ZB --connect` successfully
opens the ASB proxy from an x64 managed process; `Connect` returns true,
channel state is `Opened`, and `PublishWriteComplete` returns success with
zero pending writes.
- `AsbProxyProbe --access=ZB --connect --register --read --write-int=401
--tag=TestChildObject.TestInt` proves the direct ASB value path. The tag
registers with item id `18446462598732840961`, reads as ASB type `4`
(`Int32`) value `334`, accepts a write of `401`, and reads back `401`. The
immediate write result is global success with per-item
`0x0000001F` (`OperationWouldBlock`), then `PublishWriteComplete` returns
`0x00000020` (`PublishComplete`) with the submitted write handle
`0xA5B20001` and final per-item success.
- A follow-up x86 `MxDataConsumerProbe --namespace=ZB` run hung inside the COM
wrapper and was stopped. Direct ASB proxy probing is now the preferred path
for validating data-service functionality and any future OperationComplete
trigger without relying on the standalone mixed-mode DataConsumer COM object.
- Pure .NET 10 x64 ASB port update:
`MxAsbClient.Probe --tag=TestChildObject.TestInt --write-int=412` now
completes the core register/read/write/complete data-service flow without
AVEVA assembly references. It connects/authenticates through ASB system auth,
retries the observed one-way `AuthenticateMe` startup race in `RegisterItems`,
reads the tag, accepts the write with immediate per-item `0x0000001F`, reads
back `412`, and decodes `PublishWriteComplete` result `0x00000020`, count
`1`, the submitted handle, and final per-item success. The saved evidence is
`analysis\proxy\mxasbclient-probe-stage21-register-retry.txt`.
- Pure .NET 10 ASB unregister update:
`MxAsbClient.Probe --tag=TestChildObject.TestInt` now calls
`UnregisterItems` with the registered item identity. The pure client and
installed `ASBDataV2Proxy` compare run both return global success
`0x00000000` and per-item `0x0000000B` (`OperationFailed`) for this provider.
Evidence:
`analysis\proxy\mxasbclient-probe-stage23-unregister-id.txt` and
`analysis\proxy\asbproxyprobe-unregister-compare.txt`.
- Pure .NET 10 ASB multi-item update:
`MxAsbClient.Probe --tag=TestChildObject.TestInt
--tag=TestMachine_001.TestHistoryValue` registers and reads both tags in
two-item requests. Both tags return per-item register/read success; values
decode as ASB `TypeInt32` previews `412` and `303`. Evidence:
`analysis\proxy\mxasbclient-probe-stage24-multi-read.txt`.
## Next capture steps
1. Decode `NmxSvcps.dll` and `WWProxyStub.dll` to recover interface names,
opnum signatures, and NDR stub layouts for the observed DCE/RPC UUIDs.
2. Extract all localhost binary streams, not just port `49704`, and correlate
them to harness method/callback timestamps.
3. Trace `LmxProxy.dll`, `Lmx.dll`,
`NmxAdptr.dll`, and `NmxSvc.exe` with API Monitor around:
`send`, `recv`, `WSASend`, `WSARecv`, `INmx4.PutRequest2`,
`INmx4.GetResponse2`, `IDataClient.RegisterItems2`, `IDataClient.Write2`,
and `IDataClient.PublishWriteComplete2`.
4. Decide whether a future strict MXAccess COM-compatibility mode should
intentionally reproduce the x86 `SAFEARRAY VT_BOOL` value-pairing behavior
from capture `098`; the native managed path currently uses direct
per-element bool encoding.
5. Build managed encoder/decoder tests from the scalar, array, and write-mode
matrix TSVs.
6. Decode DCE/RPC/NDR and mixed-stream records structurally; raw byte searching
has confirmed that adapter bodies are not copied verbatim to TCP.
+498
View File
@@ -0,0 +1,498 @@
# Current Sprint State
Last updated: 2026-04-26 local VM time.
## Build Status
- `MxNativeCodec.Tests`: passed.
- `MxNativeClient.Tests`: passed, including compatibility surface and recovery
lifecycle event checks.
- `MxTraceHarness`: Release build passed.
- `MxNativeClient.Probe`: Release build passed, including
`--probe-session-recover-multi`.
- `MxDataConsumerProbe`: Release build passed. It is an x86 .NET Framework
probe for `aaMxDataConsumer.dll` / `IDataConsumer`.
- `AsbProxyProbe`: Release x64 .NET Framework build passed. It directly loads
the MSIL ASB IData proxy/contract assemblies and proves x64 managed
discovery, connection, register, read, write, readback, and published write
completion against the live MxDataProvider. It also has an unregister compare
probe for installed-proxy parity.
- `MxAsbClient.Probe`: Release .NET 10 x64 build passed. It is a pure managed
ASB client slice with no AVEVA assembly references. It reads the ASB solution
shared secret through DPAPI, performs the ASB system-auth handshake, opens the
net.tcp `IASBIDataV2` channel, and live validates register, read, write,
readback, published write completion, unregister parity, and multi-item
register/read for `TestChildObject.TestInt` plus
`TestMachine_001.TestHistoryValue`.
- `MxAsbClient.Probe`: clean Release build with zero warnings after adding an
explicit `System.Security.Cryptography.Xml` `10.0.7` package reference and
replacing the obsolete PBKDF2 constructor with `Rfc2898DeriveBytes.Pbkdf2`.
- `MxAsbClient.Tests`: passed. The test project covers ASB `Variant` factory
and decode/display round trips for bool, int, float, double, string,
datetime, duration, and the matching bool/int/float/double/string/
datetime/duration array wire payloads, `MonitoredItemValue` binary
serializer round trips for publish responses, and ASB status payload decoding
for publish quality/status mapping. It also covers the stable ASB connection
options overload, endpoint validation, and friend-only debug payload helper
visibility, plus write-completion option validation and cancellation-token
surface, subscription/monitored-item option overloads, and publish result
summary helpers. Latest focused non-live shaping tests also assert exact
defaults and validation boundaries for `AsbConnectionOptions`,
`AsbWriteOptions`, `AsbWriteCompletionOptions`, `AsbSubscriptionOptions`,
and `AsbMonitoredItemOptions`, DTO shape for write-completion/readback
results, null collection argument guards, empty/non-empty `AsbPublishResult`
helpers, derived status-summary helpers, item-status array mapping, and both
compatibility `Advise` overload signatures.
- Credential scan over `analysis`, `docs`, `src`, `captures`, and `README.md`:
no matches for the protected password patterns.
## Implemented Surface
- Public MXAccess method surface is accounted for: `Register`, `Unregister`,
`AddItem`, `RemoveItem`, `Advise`, `UnAdvise`, `Write`, `WriteSecured`,
`AuthenticateUser`, `ArchestrAUserToId`, `AddItem2`, `Write2`,
`WriteSecured2`, `Suspend`, `Activate`, `AdviseSupervisory`,
`AddBufferedItem`, and `SetBufferedUpdateInterval`.
- Public event families are represented: `OnDataChange`, `OnWriteComplete`,
`OperationComplete`, and `OnBufferedDataChange`.
- `WriteSecured2` is implemented and live validated from .NET 10 x64 for the
observed authenticated secured-write body shape.
- `WriteSecured` remains intentionally unsupported because installed MXAccess
returns `0x80004021` for the observed secured/verified paths before emitting a
value-bearing NMX body.
- Object/VARIANT timestamp overloads exist for `Write2` and `WriteSecured2`,
matching the installed interop assembly's public API shape.
- `SetHeartbeatSendInterval` is exposed and live validated through
`MxNativeClient.Probe --probe-session-heartbeat --heartbeat-ticks=5
--heartbeat-max-missed=3`.
- Explicit recovery is implemented as `MxNativeSession.RecoverConnection()` and
`MxNativeCompatibilityServer.RecoverConnection(serverHandle)`. The probe
`--probe-session-recover --tag=TestChildObject.TestInt --value=321` live
validated subscribe, recover, preserved subscription count, and write-through
against local NmxSvc.
- `RecoverConnectionAsync` adds a caller-controlled retry loop via
`MxNativeRecoveryPolicy`. Automatic write retry remains intentionally absent
because it can duplicate writes.
- Recovery lifecycle is observable through `RecoveryAttemptStarted`,
`RecoveryAttemptFailed`, and `RecoveryCompleted` on both `MxNativeSession`
and the managed compatibility facade. A live recovery probe with
`--recover-attempts=2 --recover-delay-ms=100 --value=323` observed one
started event, zero failed events, and one completed event.
- Callback, operation-status, reference-registration, unparsed-callback,
compatibility data-change, buffered-data-change, and write-complete payloads
now carry `IsDuringRecovery`. The current policy is pass-through with an
explicit marker rather than suppression or buffering.
- `MxNativeClient.Probe --probe-session-recover-multi` replays several active
subscriptions and counts recovery-window callbacks by callback family.
## Runtime Evidence Added In Latest Sprints
- GR identified `TestMachine_001.TestHistoryValue` as a deployed, historized
integer dynamic attribute.
- Captures `121` and `122` proved context-bearing buffered registration/result
bodies for `TestHistoryValue.property(buffer)` in context `TestMachine_001`.
- Both supervisory and plain advise buffered captures still produced no native
`Fire_OnBufferedDataChange` entry on this VM.
- Reflection over `ArchestrA.MXAccess.dll` confirmed the 18-method public
method surface, four event families, `MxStatus` layout, `MxStatusCategory`,
`MxStatusSource`, and full `MxDataType` enum values.
- Installed `Lmx.aaDCT` detail text has been folded into `MxStatusDetails` for
details `16`-`61`, `541`, `542`, and `8017`.
- Explicit recovery event reporting is implemented and live validated. The
events report attempt number, maximum attempts, exception, and retry intent
for failed attempts.
- A live multi-subscription recovery probe replayed four subscriptions and
preserved all four. It observed two data callbacks and four unparsed callbacks
overall, but zero callbacks with `IsDuringRecovery=true` on this VM.
- A follow-up churn run with `--recover-concurrent-writes` wrote
`TestChildObject.TestInt` values `330`-`334` from a separate managed session
during recovery. It preserved all four subscriptions and observed four data
callbacks overall, including two with `IsDuringRecovery=true`. This validates
the pass-through-with-marker callback policy under live traffic.
- `aaMxDataConsumer.dll` was imported with `tlbimp` into
`analysis\interop\Interop.aaMxDataConsumer.dll` and decompiled under
`analysis\decompiled-interop\Interop.aaMxDataConsumer`. Registry CLSID
`{85209FB2-0BA1-4594-BBC4-59D3DDAB823D}` maps to `MxDataConsumer Class`.
- `MxDataConsumerProbe` can instantiate `DataConsumerClass`, register
`IDataConsumerCallback`, resolve namespace strings to namespace ID `1`, and
call `ResolveReference`, `subscribe`, `ActivateSuspend`, and
`ProcessActivateSuspend2`. However, `IsConnected(namespaceId)` remains `0`
for tested namespace strings (`Galaxy`, `ArchestrA`, `Lmx`, `localhost`,
`ZB`), no registration/subscription responses are produced, and
`ProcessActivateSuspend2` returns `0x8007139F` (`ERROR_INVALID_STATE`).
- `DataClientClass` appears in the imported `aaMxDataConsumer` type library,
but its CLSID `{73BC4121-FF89-4762-901C-206E2BD9FE87}` is not registered on
this node; `MxDataConsumerProbe --probe-dataclient` reports
`0x80040154` (`REGDB_E_CLASSNOTREG`) before endpoint tests can run.
- Headless/ILSpy analysis of `aaMxDataConsumer.dll` shows the COM consumer is a
mixed-mode wrapper around managed ASB data proxies. `CDataClientCLI` creates a
`DataClientProxy`, stores the namespace string, starts an auto-connect
worker, and `DataClientProxy.Initialize` passes that same string as
`AccessName` to `IDataProxySelector.SelectProxyForLatestEndpoint`.
- `ASBIDataV2Adapter.dll` from the GAC contains `IDataProxySelector`. It first
calls `ASBDataV2Proxy.FindIDataEndpoint(accessName, DiscoveryScope.Global)`;
if that returns endpoints it constructs `ASBDataV2Proxy`, otherwise it falls
back to V1. The discovery scope is
`domainname/<accessName>/global`.
- `AsbProxyProbe` live x64 discovery found one `IASBIDataV2` endpoint for
access name `ZB`:
`net.tcp://desktop-6jl3kko/ASBService/Default_ZB_MxDataProvider/IDataV2`.
It found no endpoint for `Default_ZB_MxDataProvider`, `Galaxy`,
`localhost`, or `ZB2`.
- `AsbProxyProbe --access=ZB --connect` opened the ASB data proxy from an x64
managed process. `ASBDataV2Proxy.Connect` returned `true`, channel state was
`Opened`, and `PublishWriteComplete` returned `0x00000000` with no pending
writes. This proves a managed x64 ASB route to LMX/NMX data services exists
on this node.
- `AsbProxyProbe --access=ZB --connect --register --read --tag=TestChildObject.TestInt`
registered and read the test integer through `IASBIDataV2`. `RegisterItems`
and `Read` both returned `0x00000000`; the item id was
`18446462598732840961`; the read value was ASB `DataType.TypeInt32` (`4`)
with payload `4E010000`, decoded as `334`.
- `AsbProxyProbe --access=ZB --connect --register --read --write-int=401 --tag=TestChildObject.TestInt`
accepted a basic ASB write and proved readback. The immediate `Write` result
was globally successful, the per-item synchronous status was `0x0000001F`
(`ArchestrAError.OperationWouldBlock`), the next read returned integer value
`401`, and `PublishWriteComplete` then returned `0x00000020`
(`ArchestrAError.PublishComplete`) with the original write handle
`0xA5B20001` and per-item completion status `0x00000000`. This is the first
direct evidence that ASB write completion polling can supply the missing
operation-completion source without the unstable DataConsumer COM wrapper.
- Forcing the x86 `MxDataConsumerProbe` to `--namespace=ZB` hung in the COM
wrapper path and was stopped. The direct ASB route is currently the cleaner
path for x64 managed implementation and for any future `OperationComplete`
trigger testing.
- The pure .NET 10 ASB port in `src\MxAsbClient` now proves the live data path
without loading AVEVA assemblies. Key auth details recovered during the port:
this VM's ASB solution registry key is `Archestra_DESKTOP-6JL3KKO`,
`HashAlgorthim=None`, `keySize=256`, and the solution overrides the DH prime
with the installed 768-bit value. The remaining compatibility fixes were the
AVEVA CLR data-contract namespaces/field ordering for `ConnectionValidator`,
`WriteValue`, `ItemStatus`, `ItemIdentity`, and `ItemWriteComplete`, plus
mutable-struct-safe binary decode for nested `Variant` and `ASBStatus`
values.
- `MxAsbClient.Probe --tag=TestChildObject.TestInt --write-int=412` completed
from .NET 10 x64. The run read the previous value `411`, accepted write
`412` with immediate per-item `0x0000001F` (`OperationWouldBlock`), read back
`412`, and decoded `PublishWriteComplete` result `0x00000020`, count `1`,
handle `0xA5B21001`, and final per-item status `0x00000000`.
- `RegisterItems` parity is resolved for the observed startup race. ASB
`AuthenticateMe` is a one-way WCF call; the first immediate `RegisterItems`
can arrive before `ASBIDataV2Shim.AuthenticateMe` adds the connection
implementation. The client now matches installed proxy signing for normal
data calls and retries `RegisterItems` briefly when the immediate result is
`0x00000001` (`InvalidConnectionId`). Stage21 evidence shows first register
message `2` returned `InvalidConnectionId`, retry message `3` returned
`0x00000000` with item id `18446462598732840961`, and later calls on the
same connection succeeded.
- `UnregisterItems` is implemented in the pure .NET 10 ASB port and compared
against the installed AVEVA `ASBDataV2Proxy`. Both clients return global
success `0x00000000` and the same per-item `0x0000000B`
(`OperationFailed`) for the registered `TestChildObject.TestInt` identity on
this provider. Evidence:
`analysis\proxy\mxasbclient-probe-stage23-unregister-id.txt` and
`analysis\proxy\asbproxyprobe-unregister-compare.txt`.
- Multi-item ASB register/read is implemented and live validated in the pure
.NET 10 port. `analysis\proxy\mxasbclient-probe-stage24-multi-read.txt`
registers and reads `TestChildObject.TestInt` and
`TestMachine_001.TestHistoryValue` in single two-item requests. Both register
and read return global `0x00000000` and per-item `0x00000000`; the read values
decode as ASB `TypeInt32` with previews `412` and `303`.
- The pure .NET 10 ASB port now has non-live ASB `Variant` construction and
decode/display coverage for the core scalar and array type matrix:
`TypeBool`, `TypeInt32`, `TypeFloat`, `TypeDouble`, `TypeString`,
`TypeDateTime`, `TypeDuration`, and their matching array forms. The public
client also exposes a generic `Write(tag, Variant, writeHandle)` path so the
next live write probes can reuse the same encoder instead of being hard-coded
to `Int32`.
- A live ASB register/read probe validated the deployed scalar and array read
type matrix from .NET 10 x64:
`TestChildObject.TestBool` -> `TypeBool` (`17`),
`TestChildObject.TestInt` -> `TypeInt32` (`4`),
`TestChildObject.TestFloat` -> `TypeFloat` (`8`),
`TestChildObject.TestDouble` -> `TypeDouble` (`9`),
`TestChildObject.TestString` -> `TypeString` (`10`),
`TestChildObject.TestDateTime` -> `TypeDateTime` (`11`), and the matching
`TestBoolArray[]`, `TestIntArray[]`, `TestFloatArray[]`,
`TestDoubleArray[]`, `TestStringArray[]`, and `TestDateTimeArray[]` returning
ASB array types `57`, `44`, `48`, `49`, `50`, and `51`. All register/read
per-item statuses were `0x00000000`, and previews decoded correctly.
- `MxAsbClient.Probe` now keeps SOAP envelopes and custom serializer byte dumps
opt-in through `--dump-messages`; normal traced runs still show stages,
statuses, type IDs, payload lengths, timestamps, and decoded previews without
dumping request/response bodies.
- Pure .NET 10 ASB scalar writes are live validated beyond `Int32`:
`TestBool` accepted a `TypeBool` write and later read back `False` then
`True`; `TestFloat` wrote/read `13.25`; `TestDouble` wrote/read `13.75`;
`TestString` accepted `asb-scalar-write` and read it back on a delayed
follow-up read; `TestDateTime` wrote/read `2026-04-26T14:33:00Z`.
These runs confirm the scalar factory payloads are accepted by the live
provider. Immediate `PublishWriteComplete` often returned count `0`, and
bool/string immediate readback could be stale or time out, so completion
polling/readback timing remains part of the production timeout policy gap.
- Pure .NET 10 ASB array writes are live validated for the deployed dynamic
array tags. `TestIntArray[]` accepted/read back `11`-`20`;
`TestBoolArray[]` accepted/read back alternating false/true values;
`TestFloatArray[]` and `TestDoubleArray[]` accepted/read back
`11.1`-`20.1`; `TestStringArray[]` accepted/read back `K11`-`T20`; and
`TestDateTimeArray[]` accepted/read back ten UTC timestamps from
`2026-04-26T14:40:00Z` through `2026-04-26T14:49:00Z`. Int, bool, and string
array readback needed a delayed follow-up read, matching the scalar timing
behavior noted above.
- Production cleanup removed the active ASB build warnings: the vulnerable
transitive `System.Security.Cryptography.Xml` `10.0.0` package is overridden
with `10.0.7`, `dotnet list package --include-transitive --vulnerable` now
reports no vulnerable packages for `MxAsbClient`, and the system-auth
PBKDF2 derivation uses the non-obsolete static API while preserving the
installed proxy's 1000-iteration SHA1 derivation inputs. A live ASB read of
`TestChildObject.TestInt` still succeeds after the authenticator change.
- Pure .NET 10 ASB subscription/publish is now live-proven for the basic
lifecycle. `MxAsbClient.Probe --subscribe --publish-count=3` created
subscription id `3`, added monitored items for `TestChildObject.TestInt` and
`TestChildObject.TestString` with per-item status `0x00000000`, received both
values on the second publish poll (`TypeInt32` preview `412` and `TypeString`
preview `asb-scalar-write`), deleted the subscription with global success,
and then completed the existing unregister cleanup path. Publish responses
returned global `0x00000020`, matching the previously observed completion
queue success signal, so exact result/status mapping remains open.
- The ASB publish path now maps raw `MonitoredItemValue` responses into
callback-ready values with item-id-to-tag-name resolution, decoded value,
UTC timestamp, `MxQuality`, and parsed ASB status elements. A live publish
run for `TestChildObject.TestInt` and `TestChildObject.TestString` decoded
quality `0x00C0` and status elements
`OpcUaStatus:0|OpcUaVendorStatus:0|MxStatusCategory:0|MxStatusDetail:0|MxQuality:192`
for both values. The same run added `DeleteMonitoredItems`, returned global
success and per-item `0x00000000` for both monitored identities, then deleted
the subscription successfully.
- `MxAsbDataClient` now exposes `PublishedValueReceived` plus `PublishValues`.
`PublishValues` keeps the raw `PublishResponse`, maps values through the
client's monitored-item id cache, and raises one event per mapped value. A
live probe observed `published_event[0]` for `TestChildObject.TestInt` and
`published_event[1]` for `TestChildObject.TestString`, both with quality
`0x00C0` and expected previews.
- `MxAsbCompatibilityServer` now adapts the pure ASB stream into an MXAccess-like
server/item-handle data-change facade. A live
`MxAsbClient.Probe --compat-subscribe --publish-count=3` run registered ASB
server handle `1`, added/advised item handles `1` and `2`, polled publish,
raised two `compat_data_change` events with values `412` and
`asb-scalar-write`, quality `0x00C0`, decoded ASB status elements, removed
both monitored items, and unregistered cleanly.
- The compatibility facade now has an `Advise` overload that accepts
`AsbSubscriptionOptions` plus `AsbMonitoredItemOptions`, matching the public
ASB subscription shaping instead of forcing positional primitives. The probe
compatibility subscribe path now exercises this route, and a live
compatibility subscribe run through the options overload registered/advised
item handles, published mapped `compat_data_change` events, removed monitored
items, and unregistered cleanly.
- Initial non-success ASB item-status mapping is implemented. A live probe for
`DefinitelyMissingObject.DefinitelyMissingAttribute` showed global register
and read success but per-item read error `0x000A`
(`InvalidMonitoredItems`), an empty value with value-status payload decoded
as `OpcUaStatus:32905|OpcUaVendorStatus:0|MxStatusCategory:750|MxStatusDetail:0|MxQuality:0`,
and unregister per-item error `0x000B` (`OperationFailed`). Unknown payloads
are still preserved rather than guessed.
- `MxAsbClient.Probe --probe-error-cases` now keeps broader live ASB
non-success evidence opt-in without changing production client behavior.
Evidence in `analysis\proxy\mxasbclient-probe-error-cases-v2.txt` covers:
invalid read target global success with per-item `0x000A`
(`InvalidMonitoredItems`) plus bad value status; invalid-target async write
immediate per-item `0x001F` (`OperationWouldBlock`) followed by
`PublishWriteComplete` global `0x0020` (`PublishComplete`) and completion
per-item `0x0006` (`MonitoredItemsNotFound`) with status payload
`OpcUaStatus:6|OpcUaVendorStatus:0|MxStatusCategory:4|MxStatusDetail:6`;
wrong-type string write to `TestChildObject.TestInt` immediate per-item
`0x001F`, completion per-item `0x0013`
(`WriteFailedBadTypeMismatch`) with status payload
`OpcUaStatus:19|OpcUaVendorStatus:0|MxStatusCategory:4|MxStatusDetail:8001`,
and unchanged readback value `412`; invalid monitored-item cleanup global
success with per-item `0x000B` (`OperationFailed`); invalid subscription
delete global `0x000C`, status `0x0020`, specific `0x80020000`; and empty
subscription publish returning global `0x0020` with no values.
- ASB result/status summaries now name the full installed `ArchestrAError`
code set, classify publish `0x0020` (`PublishComplete`) as success-like for
publish polling, summarize `MxQuality` as good/uncertain/bad/unknown, and map
known MX status details `16` and `17` to `RequestTimedOut` and
`BadNoCommunication` while preserving raw category/detail/quality values.
- Async ASB write completion now has a production-oriented polling helper.
`WaitForWriteComplete` polls `PublishWriteComplete` for a target write
handle until completion or timeout, preserving every raw poll response and
completion record. `WaitForWriteCompleteAndRead` adds optional delayed
readback without retrying or duplicating the write. A live probe using
`--write-int=412 --wait-write-complete --write-complete-timeout-ms=5000
--write-complete-poll-ms=250 --write-readback-delay-ms=500` saw the first
completion poll return count `0`, the second poll return the matching
handle `0xA5B21001`, and readback return `412`.
- ASB client disposal now has explicit cleanup reporting. `Dispose()` delegates
to idempotent `Cleanup()`, sends signed `Disconnect` best-effort, closes the
channel and factory independently, aborts safely after close/fault failures,
and records `LastCleanupResult` with `Completed`, `Succeeded`,
`UsedAbortFallback`, and `RequiresNewConnection`. A live read probe ended
with `asb.stage=cleanup`, `asb.stage=disconnect`, and
`asb.cleanup.succeeded=True`.
- ASB reconnect now has an explicit opt-in API. `MxAsbDataClient.Reconnect`
validates caller retry options, optionally cleans up the current connection,
returns a new client on success, and preserves attempt plus cleanup evidence
without silently replaying reads or writes. A live
`MxAsbClient.Probe --probe-reconnect --reconnect-attempts=2
--reconnect-delay-ms=250 --cleanup-disconnect-timeout-ms=5000
--cleanup-close-timeout-ms=5000` run read `TestChildObject.TestInt` as
`412`, cleaned up and disconnected the original connection with caller
cleanup timeouts, connected successfully on attempt `1`, re-registered the
tag on the new client, read back `412`, and completed final cleanup
successfully.
- ASB cleanup partial-failure behavior is now covered in focused local tests
without forcing live provider faults. The close-or-abort policy is factored
into `AsbCommunicationCleanup`, and tests cover already-closed objects,
faulted objects that skip close and abort directly, close failure followed by
abort fallback, abort failure reporting, and propagation of the configured
close timeout.
- ASB cleanup cancellation now has explicit policy. `AsbClientCleanupOptions`
accepts a `CancellationToken`; when cancellation is already requested, cleanup
skips graceful disconnect/close attempts and uses local abort cleanup for
non-closed channel/factory objects. Existing disconnect and close timeouts
still bound synchronous WCF calls that have already started. A live
`MxAsbClient.Probe --probe-canceled-cleanup` run connected, registered, and
read `TestChildObject.TestInt` as `412`, then skipped disconnect and aborted
both opened communication objects locally. The cleanup result completed with
`succeeded=False`, `abort=True`, `requires_new=True`, and an
`OperationCanceledException` disconnect marker.
- Safe connection-failure evidence is now captured without destabilizing the
live ASB provider. `MxAsbClient.Probe --probe-connect-failure
--endpoint=net.tcp://localhost:1/ASBService/Default_ZB_MxDataProvider/IDataV2`
reports `connect_failure_observed=True`, an `EndpointNotFoundException`, and
inner `SocketException` `10061` for a refused local TCP endpoint. `Connect`
now also closes or aborts any WCF channel/factory objects created before a
connection setup failure; the refused-endpoint probe reports
`asb.stage=connect-cleanup` before surfacing the exception.
- OperationComplete candidate evidence was narrowed on the direct ASB route.
The `IASBIDataV2` contract exposed by the live endpoint has no activate,
suspend, or generic operation-complete operation; only `PublishWriteComplete`
exists, and it is write-specific. A live
`MxAsbClient.Probe --probe-operation-complete-candidates` run against
`TestChildObject.TestInt` and `TestChildObject.TestString` polled
`PublishWriteComplete` before and after create-subscription, add-monitored,
publish, delete-monitored, and delete-subscription operations. Every poll
returned count `0`, while the non-write operations themselves succeeded.
- ASB buffered-subscription probing is now explicit. `MxAsbDataClient` exposes
the `MonitoredItem.Buffered` flag through `AddMonitoredItems`, and
`MxAsbClient.Probe --subscribe-buffered` sets it. A live buffered ASB publish
probe against the historized `TestMachine_001.TestHistoryValue` succeeded,
but returned the same single current `MonitoredItemValue` shape as the normal
publish path: value `303`, quality `0x00C0`, timestamp
`2026-04-26T02:33:45.4260000Z`, and no multi-sample buffered batch.
- ASB status mapping now includes installed MX detail `33`
(`WriteAccessDenied`) and maps it to `WriteFailedAccessDenied`. Tests cover
success, invalid monitored item, wrong-type write completion, invalid cleanup
evidence inputs, request timeout detail `16`, platform/no-communication
detail `17`, and access-denied detail `33`.
- Safe access-denied/no-communication candidate probes did not produce those
source conditions on this VM. Same-value direct ASB writes to secured
`TestMachine_001.ProtectedValue` and verified
`TestMachine_001.ProtectedValue1` both completed successfully and read back
unchanged, so direct system-auth ASB writes do not reproduce the MXAccess
public secured-write rejection path here. A read-only sweep of
`TestMachine_001.ProtectedValue` through `TestMachine_020.ProtectedValue`
registered and read all 20 values with good quality `0x00C0`; no
no-communication status was observed.
- Native NMX completion-only operation-status handling is now stricter. One-byte
completion-only frames remain preserved as raw, unpromoted completion statuses
even when the byte is `0x00`; only the observed 5-byte status-word frame
`00 00 50 80 00` maps to `MxStatus.WriteCompleteOk` and can raise the
MXAccess-compatible write-complete event. Tests cover completion-only
`0x00`, `0x41`, and `0xEF`, plus the promoted `0x8050/0x00` status-word
frame. A search of the available Ghidra/decompiled outputs did not expose a
completion-byte-to-`MXSTATUS_PROXY[]` mapping table beyond this public-event
evidence.
- Public ASB connection shaping has started. `AsbConnectionOptions` is now the
stable options object for `MxAsbDataClient.Connect`, the previous string
overload delegates to it, and `MxAsbCompatibilityServer.Register` has a
matching options overload. Endpoint validation now happens before registry or
WCF setup work. `AsbPayloadDebug` is no longer part of the public surface and
is friend-only for the test/probe assemblies. A clean Release probe build,
non-live ASB contract test run, and live read of `TestChildObject.TestInt`
all passed after this change.
- ASB write-completion options now validate their own polling/readback policy
and expose a `CancellationToken`. Cancellation is checked before each
`PublishWriteComplete` poll and during poll/readback delays; already-running
synchronous ASB calls remain bounded by the existing WCF operation behavior.
A live same-value write-completion probe still completed on the second poll
for handle `0xA5B21001` and read back `412`.
- ASB subscription API shaping now has `AsbSubscriptionOptions` and
`AsbMonitoredItemOptions` overloads while preserving the existing positional
overloads. A live subscribe/publish probe created subscription `15`, added
monitored items for `TestChildObject.TestInt` and
`TestChildObject.TestString`, published both mapped values/events, deleted
the monitored items, deleted the subscription, and cleaned up the ASB
connection successfully.
- `AsbPublishResult` now exposes a derived `Result` summary and `HasValues`
helper so callers do not have to remap raw `PublishResponse.Result` on every
publish poll. The existing raw response and mapped values remain preserved.
- Public API/request-shaping tests were expanded without live ASB dependency.
They now pin defaults, validation, overload visibility, and helper DTO shape
for connection, write completion/readback, subscription, monitored-item,
compatibility `Advise`, and publish-result surfaces.
- Basic writes now also have an `AsbWriteOptions` overload carrying the write
handle and optional comment. The existing positional write overload and
`WriteInt32` path delegate through it, preserving behavior; a live same-value
write-completion probe still completed on the second poll and read back
`412`.
- Public multi-item APIs now explicitly reject null collection arguments before
touching client state: `RegisterMany`, `UnregisterMany`, `ReadMany`,
option-based `AddMonitoredItems`, and `DeleteMonitoredItems`. Empty
collections and tag/item contents keep their prior behavior.
- Published values and compatibility data-change events now expose derived
`StatusSummary` helpers, and `AsbResultMapper.ToItemSummaries` maps nullable
item-status arrays into raw-preserving summaries with null/empty as an empty
result.
- `docs\ASB-Variant-Wire-Format.md` now captures the supported ASB `Variant`
wire format: type IDs, scalar and array payload layouts, timestamp/duration
handling, string encoding, runtime value wrapping, quality/status payload
parsing, and intentionally raw-preserved unsupported types.
- `docs\ASB-Native-Integration-Decision.md` now records the recommended
integration boundary: prefer `MxAsbClient` for the regular tag data plane
behind a higher-level compatibility routing facade, keep `MxNativeClient` for
native callback-only semantics and not-yet-proven MXAccess behaviors, and
avoid merging ASB WCF lifecycle internals into the NMX DCE/RPC session.
## Remaining Highest-Value Gaps
1. Additional live faulted-channel and source-condition evidence that can be
gathered safely. Current safe secured/verified-write and protected-value
read sweeps did not produce access-denied or no-communication statuses.
2. Broaden pure .NET 10 ASB only where safe source conditions expose new
behavior. Local ASB `Variant` encode/decode, live reads/writes,
subscription/create/add/publish/delete-monitored/delete-subscription,
mapped publish events, compatibility data-change facade, options-shaped
connection/write/subscription/advise/write-completion APIs, cleanup, and
reconnect are now covered. Remaining work is mainly external failure modes
such as access denied or no-communication when a safe source condition is
available.
3. A native runtime trigger for `OperationComplete`. The direct ASB path now
proves a write-completion queue (`PublishWriteComplete`) carrying write
handles and per-item statuses. Direct `IASBIDataV2` does not expose
activate/suspend or a generic operation-complete contract, and subscription
create/add/publish/delete operations did not enqueue write-completion
records. Map to MXAccess event ID `3` only if a separate native trigger is
found.
4. A native runtime/source condition that emits `OnBufferedDataChange` sample
batches. Direct ASB buffered monitored items are accepted by this provider,
but the observed publish response still carries one current value rather
than a native MXAccess buffered sample batch.
5. Exact mapping for completion-only operation-status bytes into
`MXSTATUS_PROXY[]`; current code now preserves all completion-only bytes as
unpromoted raw statuses instead of guessing, including completion-only
`0x00`.
6. Cleanup behavior around live faulted/partial failure cases when a safe
provider condition is available.
7. Less-common ASB variant types only when real deployed tags expose them; the
currently supported/proven variant layout is documented and unsupported
declared types remain raw-preserved.
## Recommended Next Sprint
Treat the public ASB API-shaping sprint as closed unless a concrete caller gap
appears. The remaining local documentation gaps are also closed. Return to exact
completion-only operation-status mapping into `MXSTATUS_PROXY[]` only if a new
safe native public-event capture or deeper targeted decompile can provide
evidence. Keep live faulted-channel, access-denied, and source
no-communication probes opportunistic unless a safe provider condition appears.
File diff suppressed because it is too large Load Diff
+328
View File
@@ -0,0 +1,328 @@
# Ghidra headless analysis
This note captures the headless Ghidra pass used to identify native MX/LMX/NMX
function boundaries and build Frida hook targets.
## Tooling
Ghidra was reused from:
```text
C:\Users\dohertj2\Desktop\focas\tools\ghidra_12.0.4_PUBLIC
```
Java was available but not on `PATH`, so headless commands set:
```text
JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.10.7-hotspot
```
The AVEVA binaries were staged under a path without spaces:
```text
analysis\ghidra\input
```
The headless post-script is:
```text
analysis\ghidra\scripts\MxNmxExport.java
```
It exports metadata, not full decompiled source:
```text
analysis\ghidra\exports\*.ghidra.md
analysis\ghidra\exports\*.functions.tsv
analysis\ghidra\exports\*.string-refs.tsv
analysis\ghidra\exports\*.call-refs.tsv
```
## Static targets
High-value functions from `LmxProxy.dll`:
| Function | RVA | Evidence |
| --- | ---: | --- |
| `CLMXProxyServer::Write` variant A | `0x12c0c` | references `CLMXProxyServer::Write - Server Handle` |
| `CLMXProxyServer::Write` variant B | `0x13280` | references `CLMXProxyServer::Write - Server Handle` |
| `CLMXProxyServer::WriteSecured` variant A | `0x12f24` | references secured-write strings |
| `CLMXProxyServer::WriteSecured` variant B | `0x135fe` | references secured-write strings |
| `CLMXProxyServer::AdviseSupervisory` | `0x142b4` | references advise-supervisory strings |
High-value functions from `NmxAdptr.dll`:
| Function | RVA | Evidence |
| --- | ---: | --- |
| `CNmxAdapter::TransferData` | `0x10996` | references `CNmxAdapter::TransferData` strings |
| `ProcessDataReceived` | `0x112da` | references invalid NMX request/response strings |
| `CNmxAdapter::PutRequest` | `0x15169` | references `CNmxAdapter::PutRequest` strings |
| `CNmxAdapter::PutRequestEx` | `0x159c3` | references `CNmxAdapter::PutRequestEx` strings |
`LmxProxy.dll` has no direct Winsock callsites in the exported call refs. It is
COM, `VARIANT`, `BSTR`, and `SAFEARRAY` heavy. `NmxAdptr.dll` and `NmxSvc.exe`
contain the relevant NMX transport/body functions.
`NmxSvcps.dll` is confirmed as a MIDL proxy/stub DLL. Its exports call:
```text
NdrDllGetClassObject
NdrDllCanUnloadNow
NdrDllRegisterProxy
NdrDllUnregisterProxy
NdrCStdStubBuffer_Release
```
## Runtime hook result
Frida hook script:
```text
analysis\frida\mx-nmx-trace.js
analysis\scripts\run_frida_mx_trace.ps1
analysis\scripts\extract_frida_trace.py
```
Successful traces:
```text
captures\022-frida-write-test-int-sequence-106-108
captures\023-frida-write-test-int-sequence-109-111
captures\024-frida-write-test-bool-sequence
captures\025-frida-write-test-float-sequence
captures\026-frida-write-test-double-sequence
captures\027-frida-write-test-string-sequence
captures\028-frida-write-test-datetime-sequence
captures\029-frida-write-test-int-array
captures\030-frida-write-test-bool-array
captures\031-frida-write-test-float-array
captures\032-frida-write-test-double-array
captures\033-frida-write-test-string-array
captures\035-frida-write-test-datetime-array-full
captures\036-frida-write-secured-test-int
captures\037-frida-write-secured2-test-int
captures\038-frida-write-secured-protectedvalue
captures\039-frida-write-secured-verified-protectedvalue1
captures\040-frida-write-normal-secured-protectedvalue
captures\041-frida-write-normal-verified-protectedvalue1
captures\042-frida-write2-test-int-timestamp
captures\043-frida-loopback-write-test-int-115
captures\044-frida-loopback-write-test-int-123456789
```
Trace `023` proves the scalar write value is visible before the localhost
transport. Traces `024` through `028` extend that result across bool, float,
double, string, and datetime writes.
At `CLMXProxyServer::Write` variant A:
| Field | Observed value |
| --- | --- |
| `args[1]` | session handle `1` |
| `args[2]` | item handle `1` |
| `args[3]` | `0x3`, consistent with `VT_I4` |
| `args[5]` | requested int value: `0x6d`, `0x6e`, `0x6f` |
| `args[7]` | user/security id `1` |
At `CNmxAdapter::PutRequest`:
| Field | Observed value |
| --- | --- |
| `args[6]` | body size `0x28` / `40` |
| `args[7]` | body pointer |
| body offset `18` | requested int value as little-endian `int32` |
The repeated 40-byte write body shape is:
```text
37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00
00 02 <value:int32-le> ff ff 00 00 00 00 00 00
00 00 c9 14 b1 08 <write-index:int32-le>
```
At `CNmxAdapter::TransferData`, the same 40-byte body is wrapped in an 86-byte
buffer. The scalar value appears at offset `64`, which is `46 + 18`.
At `CNmxAdapter::ProcessDataReceived`, the callback/update body for the write
value is 88 bytes and carries the scalar value at offset `84`.
Generated parser output:
```text
captures\023-frida-write-test-int-sequence-109-111\frida-events.tsv
```
Value-hit summary from that TSV:
| Function | Body size | Hit |
| --- | ---: | --- |
| `CNmxAdapter::PutRequest` | `40` | `109@18`, `110@18`, `111@18` |
| `CNmxAdapter::TransferData` | `86` | `109@64`, `110@64`, `111@64` |
| `CNmxAdapter::ProcessDataReceived` | `88` | `109@84`, `110@84`, `111@84` |
## Write-body matrix
The machine-readable matrix is:
```text
analysis\frida\write-body-matrix.tsv
```
Observed write body encodings:
| Type | COM carrier | `PutRequest` | `TransferData` | `ProcessDataReceived` | Encoding notes |
| --- | --- | --- | --- | --- | --- |
| `int` | `VT_I4` / `0x3`, `args[5]` | size `40`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `int32` |
| `bool` | `VT_BOOL` / `0xb`, `args[5]` | size `37`, value offset `18` | size `83`, value offset `64` | size `85`, value offset `84` | true is `ff ff ff 00` in the write body and `ff` in the data-change body; false is `00 ff ff 00` and `00` |
| `float` | `VT_R4` / `0x4`, `args[5]` | size `40`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `float32` |
| `double` | `VT_R8` / `0x5`, `args[5]`/`args[6]` | size `44`, value offset `18` | size `90`, value offset `64` | size `92`, value offset `84` | little-endian `float64` |
| `string` | `VT_BSTR` / `0x8` | size `58` or `60`, value offset `26` | size `104` or `106`, value offset `72` | size `106` or `108`, value offset `92` | UTF-16LE string payload; body size follows string length |
| `datetime` | `VT_DATE` / `0x7`, `args[5]`/`args[6]` | size `86`, value offset `26` | size `132`, value offset `72` | size `98`, value offset `88` | outbound write uses a UTF-16LE display string like `4/25/2026 2:30:00 AM`; callback/update uses FILETIME |
The repeated numeric write bodies show a stable 18-byte prefix, scalar slot, and
trailing status/counter fields. Variable-width types move the value to offset
`26`, leaving an 8-byte descriptor before the UTF-16LE data.
## Array write-body matrix
The machine-readable array matrix is:
```text
analysis\frida\write-array-body-matrix.tsv
```
Observed array write body encodings:
| Type | COM carrier | `PutRequest` | `TransferData` | `ProcessDataReceived` | Encoding notes |
| --- | --- | --- | --- | --- | --- |
| `int[]` | `SAFEARRAY VT_I4` / `0x2003` | size `86`, first value offset `28` | size `132`, first value offset `74` | size `134`, first value offset `94` | descriptor kind `0x42`, count `10`, width `4`, packed little-endian `int32` |
| `bool[]` | `SAFEARRAY VT_BOOL` / `0x200b` | size `66`, first value offset `28` | size `112`, first value offset `74` | size `114`, first value offset `94` | descriptor kind `0x41`, count `10`, width `2`; capture `098` confirmed the x86 COM automation path can project a requested non-repeating .NET `bool[]` into a paired/shifted VARIANT_BOOL wire payload |
| `float[]` | `SAFEARRAY VT_R4` / `0x2004` | size `86`, first value offset `28` | size `132`, first value offset `74` | size `134`, first value offset `94` | descriptor kind `0x43`, count `10`, width `4`, packed little-endian `float32` |
| `double[]` | `SAFEARRAY VT_R8` / `0x2005` | size `126`, first value offset `28` | size `172`, first value offset `74` | size `174`, first value offset `94` | descriptor kind `0x44`, count `10`, width `8`, packed little-endian `float64` |
| `string[]` | `SAFEARRAY VT_BSTR` / `0x2008` | size `256`, first string bytes at offset `41` | size `302`, first string bytes at offset `87` | size `304`, first string bytes at offset `107` | descriptor kind `0x45`; each element is a length-prefixed scalar string-style UTF-16LE record |
| `datetime[]` | `SAFEARRAY VT_DATE` / `0x2007` | size `596`, first string bytes at offset `41` | size `642`, first string bytes at offset `87` | size `214`, first FILETIME at offset `94` | outbound values are per-element UTF-16LE display strings; callback/update uses packed FILETIME records |
Array bodies use an 11-byte descriptor beginning at body offset `17`:
```text
kind_byte 00 00 00 00 element_count:uint16 element_width_or_code:uint32
```
Packed numeric array values begin at body offset `28`. String and datetime
arrays also begin their element records at offset `28`, with the first actual
UTF-16LE character at offset `41`.
## Write mode matrix
The machine-readable write-mode matrix is:
```text
analysis\frida\write-mode-matrix.tsv
```
Findings:
| Scenario | Result |
| --- | --- |
| `WriteSecured` against `TestChildObject.TestInt` | rejected before value-bearing `PutRequest` with `0x80004021` |
| `WriteSecured2` against `TestChildObject.TestInt` | rejected before value-bearing `PutRequest` with `E_INVALIDARG` |
| `WriteSecured` against `TestMachine_001.ProtectedValue` (`SecuredWrite`) | rejected before value-bearing `PutRequest` with `0x80004021` |
| `WriteSecured` against `TestMachine_001.ProtectedValue1` (`VerifiedWrite`) | rejected before value-bearing `PutRequest` with `0x80004021` |
| normal `Write` against `ProtectedValue` with fourth argument `2` | succeeds; same bool body shape as scalar bool writes |
| normal `Write` against `ProtectedValue1` with fourth argument `3` | succeeds; same bool body shape as scalar bool writes |
| `Write2` against `TestChildObject.TestInt` | succeeds; `PutRequest` size remains `40`, value stays at offset `18`, FILETIME appears at offset `24` |
Implication: for MXAccess public automation, the supported secured/verified
route is the regular `Write` method with the fourth argument set to the Galaxy
security classification. The public `WriteSecured*` methods are present in the
type library but did not produce a supported value-bearing request in these
captures.
Later authenticated captures and headless decompile refined this:
- `analysis\ghidra\exports\LmxProxy.dll.write-secured-decompile.md` decompiles
`FUN_10012f24` (`WriteSecured`) and `FUN_100135fe` (`WriteSecured2`).
- `WriteSecured` performs an extra item-record byte check at offset `0x0f` and
returns `0x80004021` when that byte is nonzero. The secured and verified bool
tags on this node hit that branch even after `AuthenticateUser` succeeds.
- `WriteSecured2` skips that item-record flag check. It resolves current and
verifier user handles to 16-byte authenticator tokens, copies both the value
and timestamp variants, and calls the downstream vtable slot that emitted the
observed NMX `0x38` body in captures `113`-`116`.
- The shared user-token lookup branch returns `0x80070057` when the user handle
is not mapped, explaining the earlier unauthenticated `WriteSecured2` failure
against `TestChildObject.TestInt`.
## Transport correlation
Combined Frida plus loopback capture:
```text
captures\043-frida-loopback-write-test-int-115
captures\044-frida-loopback-write-test-int-123456789
analysis\scripts\run_frida_loopback_capture.ps1
analysis\scripts\map_frida_to_tcp.py
captures\043-frida-loopback-write-test-int-115\frida-to-tcp-map.tsv
captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv
```
For the same write of `TestChildObject.TestInt = 115`, the mapper extracted the
exact Frida `PutRequest`, `TransferData`, and callback bodies and searched the
reassembled TCP streams. Result:
| Needle | TCP result |
| --- | --- |
| raw `int32` value `115` | present in several streams, including `::1:49704`, but surrounding bytes show DCE/RPC call IDs/metadata and unrelated product records |
| exact 40-byte `PutRequest` body | not found |
| exact 86-byte `TransferData` body | not found |
| exact 88-byte callback body | not found |
Capture `044` repeats the test with `123456789`, which avoids DCE/RPC call-ID
ambiguity. That raw scalar is absent from the full pcap payload scan, the
`::1:49704` DCE/RPC stubs, and the mixed `127.0.0.1:57415 <-> 57433` stream.
This means the native adapter bodies are not copied verbatim onto TCP, and the
write value is not exposed as a plain little-endian scalar in the transport for
the distinctive-value capture. The next transport task is to decode the
DCE/RPC/NDR layer and the mixed local stream messages structurally.
## Managed codec artifact
The first .NET 10 managed implementation artifact is:
```text
src\MxNativeCodec\MxNativeCodec.csproj
src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj
```
This is intentionally template-based. It preserves unknown tag/session/header
fields from an observed `PutRequest` body, then rewrites the typed value slot and
write index. The tests round-trip the real Frida bytes for bool, int, float,
double, string, datetime, timestamped `Write2` int, and the observed
int/bool/float/double/string array write bodies.
Verification command:
```text
dotnet run --project src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj -c Release
```
The harness also now accepts typed array writes:
```text
--type=int[] --value="1;2;3"
--type=string[] --value="A;B;C"
```
For multiple whole-array writes in one session, `--values=` can use `|` between
array values so element commas are not ambiguous.
## Implication
The prior pcap-only decode showed local stream framing but did not isolate the
application scalar. Headless Ghidra plus Frida closes that gap: the native NMX
adapter receives compact bodies where scalar and string/date values are plainly
encoded before they enter localhost transport. The next implementation task is
to turn this matrix into encoder/decoder tests, then broaden the same approach
to arrays, quality/status responses, tag binding messages, and secured-write
variants.
+267
View File
@@ -0,0 +1,267 @@
# Loopback protocol findings
This note captures the first Npcap loopback decode pass for the MXAccess to
LMX/NMX path.
## Capture set
The repeatable capture runner is:
```text
analysis\scripts\run_loopback_capture.ps1
```
It starts `dumpcap` on the Npcap loopback adapter and runs the x86 MXAccess
harness:
```text
src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe
```
Focused loopback captures:
| Folder | Scenario |
| --- | --- |
| `captures\013-loopback-subscribe-scalars` | subscribe `TestBool`, `TestInt`, `TestString` |
| `captures\014-loopback-subscribe-array-bracketed` | subscribe `TestStringArray[]` |
| `captures\015-loopback-subscribe-invalid` | subscribe invalid reference |
| `captures\016-loopback-write-test-int-advised` | advised same-value write to `TestInt` |
Per-capture generated files:
| File | Purpose |
| --- | --- |
| `loopback.pcapng` | raw Npcap loopback capture |
| `harness.log` | timestamped MXAccess method and callback log |
| `dcerpc-49704.tsv` | tshark DCE/RPC frame index for the NMX service port |
| `nmx-conversations.tsv` | extracted IPv6 conversations involving port `49704` |
| `nmx-payload-packets.tsv` | payload packet index for the selected `49704` conversation |
| `nmx-stream-*.bin` | reassembled directional payloads for the selected `49704` conversation |
| `tcp-conversations.tsv` | all TCP payload conversations ranked by byte count |
| `tcp-payload-packets.tsv` | payload packet index for the top TCP conversations |
| `tcp-stream-*.bin` | reassembled directional payloads for the top TCP conversations |
Cross-capture summaries:
```text
analysis\scripts\extract_nmx_loopback.py
analysis\scripts\extract_tcp_conversations.py
analysis\scripts\decode_tcp_payload_packets.py
analysis\scripts\decode_mixed_local_stream.py
analysis\scripts\summarize_dcerpc.py
analysis\network\dcerpc-loopback-summary.tsv
analysis\network\write-window-tcp-payloads.tsv
```
## DCE/RPC service path
Wireshark decodes the main `::1:<ephemeral> <-> ::1:49704` traffic as DCE/RPC.
The captures show these DCE/RPC interface UUIDs:
| UUID | Observed use |
| --- | --- |
| `4e0c90df-e39d-4164-a421-ace89484c602` | initial bind context, opnum `0` calls |
| `1981974b-6bf7-46cb-9640-0260bbb551ba` | altered context, main opnums `0`, `2`, `3`, `5` |
The UUIDs were not present as direct keys under the checked COM registry areas:
```text
HKCR\Interface
HKCR\Wow6432Node\Interface
HKCR\CLSID
HKCR\Wow6432Node\CLSID
HKCR\TypeLib
HKCR\Wow6432Node\TypeLib
```
This makes `NmxSvcps.dll` and `WWProxyStub.dll` high-value next targets. They
are likely proxy/stub components, but their interfaces are not exposed through
the simple COM registry lookup above.
## DCE/RPC shape
For good scalar subscribe, good array subscribe, and successful advised write,
the main `49704` DCE/RPC shape is identical:
```text
165 request/response pairs on ctx 1, opnum 3
10 request/response groups on ctx 0 opnum 0, ctx 1 opnum 0, ctx 1 opnum 2
3 request/response pairs on ctx 1, opnum 5
```
The invalid subscribe adds one extra group:
```text
176 request/response pairs on ctx 1, opnum 3
11 request/response groups on ctx 0 opnum 0, ctx 1 opnum 0, ctx 1 opnum 2
3 request/response pairs on ctx 1, opnum 5
```
Interpretation: invalid item resolution does not fail at `AddItem`; it drives
additional NMX/RPC activity during `AdviseSupervisory`, matching the harness
callbacks where invalid names return configuration errors only after advise.
## Write path split
The successful write capture has the standard main `49704` DCE/RPC conversation:
```text
::1:55840 <-> ::1:49704
803 frames, about 84 kB
```
It also has a short additional `49704` DCE/RPC conversation:
```text
::1:49768 <-> ::1:49704
192 frames, about 19 kB
```
The harness write timestamps were:
```text
2026-04-25T05:13:58.0479762Z mx.write.begin
2026-04-25T05:13:58.0489758Z mx.write.end
2026-04-25T05:13:58.2561934Z mx.event.write-complete
```
With the pcap first frame at epoch `1777094027.708322300`, this places the
write call around relative time `10.34s` and the write-complete callback around
relative time `10.55s`.
No DCE/RPC frames on port `49704` occur in the `10.30s` to `10.62s` window.
Instead, the active payload in that exact window is primarily TCP stream `0`:
```text
127.0.0.1:57415 <-> 127.0.0.1:57433
```
That stream is not decoded by Wireshark as DCE/RPC. Its payload has a compact
binary framing pattern with frequent 12-byte control messages and small
length-prefixed payloads. Example payload prefixes around the write window
include little-endian values such as:
```text
1a 00 00 00 ...
16 00 00 00 ...
22 00 00 00 ...
1e 00 00 00 ...
3f 00 00 00 ...
```
Interpretation: the native implementation probably uses DCE/RPC for service
activation/session coordination and a separate local binary channel for at
least part of the advised update/write-complete path. The managed replacement
must account for both layers.
The stream has been extracted as:
```text
captures\016-loopback-write-test-int-advised\tcp-stream-127_0_0_1_57415-to-127_0_0_1_57433.bin
captures\016-loopback-write-test-int-advised\tcp-stream-127_0_0_1_57433-to-127_0_0_1_57415.bin
```
The stream is now decoded with packet-boundary and mixed-record helpers:
```text
analysis\scripts\decode_tcp_payload_packets.py
analysis\scripts\decode_mixed_local_stream.py
analysis\scripts\analyze_write_window.py
analysis\scripts\diff_write_window_records.py
```
The mixed-record model is:
- 12-byte control records: `int32 code_or_status`, `int32 token_low`,
`int32 token_high`.
- Data records: `uint32 body_length`, followed by the data body.
- A positive control value can announce one or more following data records.
- `-1` appears as acknowledgement/status.
- `-2` appears as a bidirectional status/control marker around write windows.
Usable value-change write captures added after this decode pass:
| Folder | Result |
| --- | --- |
| `captures\017-loopback-write-test-int-100` | `TestInt` changed `99 -> 100` |
| `captures\020-loopback-write-test-int-102` | `TestInt` changed `101 -> 102` |
| `captures\021-loopback-write-test-int-sequence-103-105` | same-session writes `103`, `104`, `105` |
The detailed COM contract and managed-client implication note is:
```text
docs\NMX-COM-Contracts.md
```
## Same-session write sequence
`captures\021-loopback-write-test-int-sequence-103-105` writes three int values
through one registered/advised MXAccess session:
```text
103 at 2026-04-25T05:53:06.9746508Z
104 at 2026-04-25T05:53:07.6963047Z
105 at 2026-04-25T05:53:08.4180133Z
```
Each write produced a good data-change callback followed by a good
write-complete callback. The top local payload stream remained:
```text
127.0.0.1:57415 <-> 127.0.0.1:57433
```
The analyzer output is:
```text
captures\021-loopback-write-test-int-sequence-103-105\write-window-mixed-records.tsv
analysis\network\write-window-body-diff-021-w0-vs-w1.tsv
analysis\network\write-window-body-diff-021-w1-vs-w2.tsv
```
Within the `-0.10s` to `+0.12s` write-complete windows, the repeated records are
the same families already seen in the single-write captures:
```text
54 8f 63 40 e2 5e 31 40 ... 26-byte data body
1c 21 18 d0 c4 6f 33 bb ... 34-byte data body
98 04 33 cb 0c b4 7c 38 ... 67-byte data body
44 6b 99 d8 ec 1b bd b5 ... 52-byte data body
55 ce ff 62 b2 1b 3a 50 ... 30-byte data body
```
The raw int values `103`, `104`, and `105` were not isolated from the pcap-only
mixed stream decode. The most consistent byte changes in that layer are
counter-like fields at offset `14` in the 26-byte and 30-byte data bodies,
offset `0` in the 22-byte and 26-byte response bodies, and related token fields
in adjacent 12-byte controls. Those fields advance with the local message
sequence and should not be treated as the application value.
The application value was later isolated one layer higher with Frida hooks
placed using headless Ghidra RVAs. See:
```text
docs\Ghidra-Headless-Analysis.md
captures\023-frida-write-test-int-sequence-109-111\frida-events.tsv
```
## Plaintext result
The selected `49704` stream binaries did not contain the test tag names in
ASCII or UTF-16LE. The captured protocol is therefore not a simple plaintext
tag-reference transport.
## Current reconstruction hypothesis
The implementation path is:
```text
ArchestrA.MXAccess.dll
-> LmxProxy.dll COM in-proc server
-> local DCE/RPC to NmxSvc.exe on ::1:49704
-> local binary channels for request/callback data
-> LMX/NMX runtime and Galaxy-derived security/type metadata
```
The next useful work is to decode the proxy/stub interfaces and the local
binary stream structure, then tie decoded calls back to MXAccess harness events.
+523
View File
@@ -0,0 +1,523 @@
# MXAccess public API surface
Source of truth: `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`
Assembly identity:
- Name: `ArchestrA.MxAccess`
- Version: `3.2.0.0`
- Public key token: `23106a86e706d0ae`
- File product string: `Assembly imported from type library 'LMXPROXYLib'.`
- Processor architecture in metadata: `None`; practical use is still x86 because the COM class is a 32-bit in-proc server.
## COM class
`ArchestrA.MxAccess.LMXProxyServerClass`
- CLSID: `{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}`
- ProgID: `LMXProxy.LMXProxyServer.1`
- Version-independent ProgID: `LMXProxy.LMXProxyServer`
- Registered server: `C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll`
- Registry location: `HKCR\Wow6432Node\CLSID\{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}`
- Threading model: `Apartment`
Because the server is under `Wow6432Node` and registered as an in-process DLL,
64-bit callers cannot instantiate it directly.
## Interface lineage
`ILMXProxyServer5` extends prior versions without changing the older method
signatures:
- `ILMXProxyServer` - original register/add/advise/write/auth API.
- `ILMXProxyServer2` - adds `ArchestrAUserToId`.
- `ILMXProxyServer3` - adds `AddItem2`.
- `ILMXProxyServer4` - adds `Write2`, `WriteSecured2`, `Suspend`, `Activate`, `AdviseSupervisory`.
- `ILMXProxyServer5` - adds buffered item support.
Interface GUIDs:
- `ILMXProxyServer`: `{CCE67FB7-EAFD-4367-9212-617043BF126D}`
- `ILMXProxyServer2`: `{020A8A87-69C5-497F-A893-B629E669FBFF}`
- `ILMXProxyServer3`: `{57D006B6-F25E-4654-A81E-BCBAFD60FE59}`
- `ILMXProxyServer4`: `{9DC0D5C1-9371-4E84-86F8-7091D316A66C}`
- `ILMXProxyServer5`: `{ECEFF506-A752-46E3-9E31-0A8E257C9926}`
## Methods
| Method | Purpose inferred from API and current usage |
| --- | --- |
| `Register(string pClientName) -> int` | Opens a client session and returns an LMX server handle. |
| `Unregister(int hLMXServerHandle)` | Closes the client session. |
| `AddItem(int hLMXServerHandle, string strItemDef) -> int` | Resolves/registers an item reference and returns an item handle. |
| `RemoveItem(int hLMXServerHandle, int hItem)` | Releases an item handle. |
| `Advise(int hLMXServerHandle, int hItem)` | Subscribes to item updates. |
| `UnAdvise(int hLMXServerHandle, int hItem)` | Cancels item updates. |
| `Write(int hLMXServerHandle, int hItem, object pItemValue, int userId)` | Writes a value. In current code `userId` is used as security classification. |
| `WriteSecured(int hLMXServerHandle, int hItem, int currentUserId, int verifierUserId, object pItemValue)` | Secured/verified write path. |
| `AuthenticateUser(int hLMXServerHandle, string verifyUser, string verifyUserPsw) -> int` | Authenticates a user and returns a numeric id. |
| `ArchestrAUserToId(int hLMXServerHandle, string userIdGuid) -> int` | Maps ArchestrA user GUID to integer id. |
| `AddItem2(int hLMXServerHandle, string strItemDef, string strItemCtxt) -> int` | Adds an item with a separate context string. |
| `Write2(int hLMXServerHandle, int hItem, object pItemValue, object pItemTime, int userId)` | Timestamped write. |
| `WriteSecured2(...)` | Timestamped secured/verified write. |
| `Suspend(int hLMXServerHandle, int hItem, out MxStatus status)` | Suspends an item/reference. |
| `Activate(int hLMXServerHandle, int hItem, out MxStatus status)` | Activates a suspended item/reference. |
| `AdviseSupervisory(int hLMXServerHandle, int hItem)` | Supervisory subscription mode. This is what the existing OPC UA bridge uses. |
| `AddBufferedItem(int hLMXServerHandle, string strItemDef, string strItemCtxt) -> int` | Adds a buffered item. |
| `SetBufferedUpdateInterval(int hLMXServerHandle, int updateInterval)` | Sets buffered update cadence. |
Reflection over the installed interop assembly confirms
`LMXProxyServerClass` exposes these 18 public methods plus the four public COM
event families: `OnDataChange`, `OnWriteComplete`, `OperationComplete`, and
`OnBufferedDataChange`. `Write2` and `WriteSecured2` expose their timestamp
argument as `object`/VARIANT. The .NET 10 compatibility facade keeps typed
`DateTime` overloads and also exposes object timestamp overloads for
API-shape parity.
## Secured write capture status
The current test node still does not produce a successful dedicated
`WriteSecured` wire path:
- `captures\036-frida-write-secured-test-int`: `WriteSecured` returned
`0x80004021` before any NMX `0x37` write body was emitted.
- `captures\038-frida-write-secured-protectedvalue`: `WriteSecured` returned
`0x80004021` before any NMX `0x37` write body was emitted.
- `captures\039-frida-write-secured-verified-protectedvalue1`: `WriteSecured`
returned `0x80004021` before any NMX `0x37` write body was emitted.
- `captures\111-frida-write-secured-auth-protectedvalue` and
`captures\112-frida-write-secured-auth-verified-protectedvalue1`:
`AuthenticateUser` succeeded first, but `WriteSecured` still returned
`0x80004021` before any value-bearing body.
- `captures\037-frida-write-secured2-test-int`: `WriteSecured2` returned
`0x80070057` before any NMX write body was emitted.
Headless Ghidra output in
`analysis\ghidra\exports\LmxProxy.dll.write-secured-decompile.md` shows why
these paths split: `WriteSecured` has an extra item-record flag check that can
return `0x80004021`; `WriteSecured2` skips that check and proceeds through the
authenticated timestamped writer when the user handles are mapped.
Normal `Write` calls to the secured/verified public test attributes did emit
ordinary `0x37` write bodies and succeeded in captures `040` and `041`. That
means the managed implementation supports the successful observed path through
normal `Write`.
Authenticated `WriteSecured2` now has successful captures and a managed encoder.
Captures `113`-`116` show command `0x38` over transfer kind `Write` (`3`) for
the boolean secured/verified tags in this GR. The body carries the current-user
authenticator token before the client name and the verifier token after the
client name. Live .NET 10 x64 probes succeeded for
`TestMachine_001.ProtectedValue` with no verifier and
`TestMachine_001.ProtectedValue1` with verifier handle `1`, both through the
low-level session API and through the handle-based compatibility facade. The
facade does not synthesize `OnWriteComplete` for this path because the native
successful captures did not show a public write-complete event. Capture `117`
adds authenticated `WriteSecured2` for integer `TestChildObject.TestInt`; it
uses the same generic timestamped-prefix rule, so the managed encoder is no
longer bool-only. Live managed probes also succeeded for float, double, string,
datetime, and the scalar-array value kinds. Native captures beyond bool and int
would still be useful as additional golden fixtures, but the managed encoder is
now generic over the existing timestamped write-body support.
## User mapping capture status
The x86 `MxTraceHarness` `user-map` scenario directly calls
`CLMXProxyServer.ArchestrAUserToId`. On this test node, it does not return the
Galaxy Repository `user_profile_id`:
| Capture | Input GUID | MXAccess return |
| --- | --- | ---: |
| `captures\mxaccess-user-map-administrator.log` | `9222FBBA-53F4-457E-8B37-C93A9A250B4A` | `1` |
| `captures\mxaccess-user-map-systemengineer.log` | `626A355F-737E-45F8-9740-43372220DEAB` | `1` |
| `captures\mxaccess-user-map-defaultuser.log` | `F4A9B907-6E72-48AE-83B5-BBDE918C890F` | `1` |
| `captures\mxaccess-user-map-invalid.log` | `00000000-0000-0000-0000-000000000000` | `1` |
The managed compatibility layer mirrors this observed behavior for
`ArchestrAUserToId`. GR profile lookup remains available separately through
`GalaxyRepositoryUserResolver`.
## Authentication capture status
`captures\mxaccess-authenticate-user-administrator-empty.log` shows
`AuthenticateUser(session, "Administrator", "")` returning user ID `1`.
Frida captures `087-frida-authenticate-administrator-empty` and
`088-frida-authenticate-invalid-empty` show both `Administrator` and
`DefinitelyNotAUser` returning `S_OK` and user ID `1` when the password is
empty on this dev node. The Frida hook records only password length, not
password content.
Headless Ghidra decompile `analysis\ghidra\exports\LmxProxy.dll.auth-decompile.md`
shows `AuthenticateUser` calling the underlying user-authenticator object and,
when it receives a security token, incrementing a per-session user counter and
mapping that generated integer to the token GUID. This matches
`ArchestrAUserToId`: the public return value is a session-local MXAccess handle,
not the GR `dbo.user_profile.user_profile_id`.
`MxNativeCompatibilityServer.AuthenticateUser` now mirrors the observed dev-node
behavior by validating the server handle and returning a session-local user
handle. It deliberately ignores and does not retain password material. A
security-enabled Galaxy still needs dedicated captures before password/hash
verification can be implemented safely.
## AddItem2 context capture status
`captures\mxaccess-additem2-testint-context.log` shows
`CLMXProxyServer.AddItem2(session, "TestInt", "TestChildObject")` succeeding and
returning item handle `1`. That confirms the simple relative-reference form used
by `MxNativeCompatibilityServer.AddItem2`: combine context and item definition
as `TestChildObject.TestInt` before GR resolution. More complex context strings
still need captures.
## Advise capture status
`captures\mxaccess-plain-advise-testint.log` shows the public
`CLMXProxyServer.Advise(session, item)` method succeeding for
`TestChildObject.TestInt`. Frida capture `099-frida-plain-advise-testint`
captures the lower-level body: plain `Advise` sends the same item-control
`0x1f` body shape as the earlier `AdviseSupervisory` capture for the same
scalar tag, including the same trailing option value `3`. The managed
compatibility layer therefore routes both public methods through the same
decoded subscription body for the observed scalar path.
## Suspend/Activate capture status
`captures\mxaccess-suspend-testint.log` and
`captures\mxaccess-activate-testint.log` show the public MXAccess methods
throwing `0x80070057` for `TestChildObject.TestInt` before a usable `MxStatus`
is returned. These captures establish that the test integer attribute is not a
valid suspend/activate scenario. A successful capture against the correct item
class is still required before implementing native NMX bodies for these methods.
## Buffered item capture status
The x86 harness now covers the public buffered APIs:
- `captures\mxaccess-set-buffered-interval-1000.log` shows
`SetBufferedUpdateInterval(session, 1000)` succeeding.
- `captures\mxaccess-add-buffered-testint-context.log` shows
`AddBufferedItem(session, "TestInt", "TestChildObject")` succeeding and
returning item handle `1`.
- `captures\mxaccess-add-buffered-write-testint-context.log` shows that using
the buffered item handle with normal `Write` throws `0x80070057`.
- `captures\079-frida-add-buffered-advise-testint` and
`captures\080-frida-buffered-external-write-testint` show the advised
buffered registration body. MXAccess does not send the normal `0x1f`
item-control advise for the buffered handle. It sends an item-control `0x10`
reference registration for `TestInt.property(buffer)` in context
`TestChildObject`, wrapped in a `MessageKind=2` transfer envelope.
- `captures\121-frida-buffered-history-testhistoryvalue-context` and
`captures\122-frida-buffered-history-testhistoryvalue-plainadvise` repeat the
buffered registration against GR-confirmed historized integer attribute
`TestMachine_001.TestHistoryValue`. Both supervisory and plain advise forms
emit the same context-bearing `0x10`/`0x11` registration/result shape for
`TestHistoryValue.property(buffer)` in context `TestMachine_001`.
Separate writer-session writes succeed and normal writer data callbacks are
observed, but native MXAccess still does not enter `Fire_OnBufferedDataChange`
on this VM.
- `captures\085-frida-subscribe-property-buffer` and
`captures\086-frida-write-property-buffer` show that adding the literal item
`TestChildObject.TestInt.property(buffer)` does not enter the public
`AddBufferedItem` helper. It follows the normal add/advise path, sends the
same item-control `0x10` reference-registration body with an empty item
context, and receives an NMX registration-result frame carrying the runtime
internal-error text for `TestInt.property(buffer)`. Normal `Write` against
the literal handle returns from `CLMXProxyServer.Write` without producing an
observed buffered callback.
- Ghidra decompile of `LmxProxy.dll` function `1001121d` confirms the public
`AddBufferedItem` implementation allocates a BSTR copy of `strItemDef`,
appends `.property(buffer)`, calls the normal add-item implementation, and
marks the resulting item record as buffered. Function `1000fc80` confirms
`SetBufferedUpdateInterval` rejects intervals below `1` and stores the
interval as `(milliseconds + 99) / 100`, effectively rounding up to 100 ms
ticks.
- Ghidra decompile of `100163c0` confirms `OnBufferedDataChange` is fired
through the `_ILMXProxyServerEvents2` connection point with seven arguments:
server handle, item handle, data type, value variant, quality variant,
timestamp variant, and status array.
- Headless xref/decompile output shows `Fire_OnBufferedDataChange` is reached
from the same native `OnDataChange callback received` method used for normal
data changes. The callback looks up the item record and branches on the
buffered item flag: non-buffered items fire `OnDataChange`; buffered items
convert the callback value into value, quality, and timestamp SAFEARRAY
variants before firing `OnBufferedDataChange`.
- The buffered value conversion helper maps MX buffered element type `1` to a
`VT_BOOL` value array, `2` to `VT_I4`, `3` to `VT_R4`, `4` to `VT_R8`, `5`
to `VT_BSTR`, and `6`/`7` to `VT_UI8` FILETIME-style values. Quality is
emitted as a `VT_I2` SAFEARRAY, timestamp as a `VT_UI8` SAFEARRAY, and the
status argument uses the normal `MXSTATUS_PROXY[]` SAFEARRAY.
No live `OnBufferedDataChange` payload has been observed yet. A source/runtime
condition that actually delivers buffered sample batches is still needed to
validate the wire body and multi-sample parser. The managed library now
implements the decoded add/registration surface, including context-bearing
buffered registrations, and routes parsed buffered callbacks to a separate
managed buffered event instead of the normal data-change event.
## Events
Event source interfaces:
- `_ILMXProxyServerEvents`: `{848299B6-DD61-4A0D-A304-3947A564B89C}`
- `_ILMXProxyServerEvents2`: `{C70A6FC4-09EF-4F31-8874-A049FEE87A95}`
Events:
```csharp
void OnDataChange(
int hLMXServerHandle,
int phItemHandle,
object pvItemValue,
int pwItemQuality,
object pftItemTimeStamp,
ref MXSTATUS_PROXY[] pVars);
void OnWriteComplete(
int hLMXServerHandle,
int phItemHandle,
ref MXSTATUS_PROXY[] pVars);
void OperationComplete(
int hLMXServerHandle,
int phItemHandle,
ref MXSTATUS_PROXY[] pVars);
void OnBufferedDataChange(
int hLMXServerHandle,
int phItemHandle,
MxDataType dtDataType,
object pvItemValue,
object pwItemQuality,
object pftItemTimeStamp,
ref MXSTATUS_PROXY[] pVars);
```
Ghidra decompile of `LmxProxy.dll` event helpers confirms that
`Fire_OnWriteComplete` and `Fire_OperationComplete` both construct a three
element `VARIANT` argument array containing server handle, item handle, and one
`MXSTATUS_PROXY` SAFEARRAY. `Fire_OnWriteComplete` dispatches event ID `2`;
`Fire_OperationComplete` dispatches event ID `3`. In the capture set, successful
writes raise only `OnWriteComplete`; no `mx.event.operation-complete` line has
been observed yet.
Headless Ghidra xref/decompile output in
`analysis\ghidra\exports\LmxProxy.dll.event-xrefs.md` and
`analysis\ghidra\exports\LmxProxy.dll.event-callers-decompile.md` narrows the
source further: `Fire_OnWriteComplete` is reached from
`CUserConnectionCallback::OnSetAttributeResult`, while `Fire_OperationComplete`
is reached from `CUserConnectionCallback::OperationComplete`. They are distinct
callback paths even though their COM event payload shape is the same.
The installed interop assemblies expose the lower-level callback as
`IMxCallback2.OperationComplete(int lCallbackId, ref MxStatus, string)`. They
also expose `IDataConsumer.ActivateSuspend` and `ProcessActivateSuspend2`, whose
response type is `ItemActiveResponse`. That makes the DataConsumer
activate/suspend completion path the strongest remaining native trigger
candidate. The public `LMXProxyServerClass.Suspend` and `Activate` methods
tested in captures `118` and `119` instead decompile to direct
`IMxScanOnDemand` calls and did not enter
`CUserConnectionCallback.OperationComplete` on this node.
`aaMxDataConsumer.dll` is registered separately as `MxDataConsumer Class`
(`{85209FB2-0BA1-4594-BBC4-59D3DDAB823D}`) and exposes the same
`IDataConsumer` activate/suspend methods through its type library. A targeted
x86 probe can instantiate it and call those methods, but the standalone object
currently reports `IsConnected(namespaceId)=0` and
`ProcessActivateSuspend2` returns `0x8007139F`. That means the COM surface is
reachable, but a DataConsumer/DataClient bootstrap step is still missing before
it can prove the public `OperationComplete` trigger.
The mixed-mode `aaMxDataConsumer.dll` decompile shows that bootstrap should
ultimately route through managed ASB IData proxies: `CDataClientCLI` owns a
`DataClientProxy`, starts an auto-connect worker, and passes the namespace
string as an ASB access name to
`IDataProxySelector.SelectProxyForLatestEndpoint`. `ASBIDataV2Adapter.dll`
contains that selector; it looks for `IASBIDataV2` endpoints under
`domainname/<accessName>/global` before falling back to IData V1. A new x64
managed probe found and connected to the live `IASBIDataV2` endpoint with
access name `ZB`, then successfully called `PublishWriteComplete`. This does
not prove `OperationComplete` yet, but it proves the relevant data-service
route can be reached from managed x64 code without MXAccess or COM.
The same x64 ASB probe now proves the register/read/write/complete flow for
`TestChildObject.TestInt`. `RegisterItems` and `Read` succeed, ASB type `4`
decodes as `Int32`, `Write(401)` is accepted with per-item
`OperationWouldBlock`, the next read returns `401`, and
`PublishWriteComplete` returns the submitted write handle with final per-item
success. That gives a concrete managed completion queue to model for ASB-native
write-complete behavior; `OperationComplete` should still not be synthesized
until an operation other than a basic write is proven to use the native
MXAccess event ID `3` path.
The same core flow is now reproduced by the pure .NET 10 x64
`src\MxAsbClient` implementation with no AVEVA assembly references. Its live
probe reads the ASB solution secret through DPAPI, performs the system-auth
handshake, reads `TestChildObject.TestInt`, writes a new integer value, reads
that value back, and decodes `PublishWriteComplete` result `0x00000020` with
the submitted write handle and final per-item success. `RegisterItems` now
matches the observed ASB startup behavior: the first immediate call after
one-way `AuthenticateMe` can return `0x00000001`, so the client retries briefly
and receives the expected item status/id once the server-side implementation is
registered. The remaining ASB-native work is API breadth: multi-item calls,
subscriptions, scalar/array type matrix, and status/error mapping.
`UnregisterItems` is now also implemented and compared against the installed
AVEVA `ASBDataV2Proxy`. Both the pure .NET 10 client and installed proxy return
global success for the unregister call and the same per-item `0x0000000B`
(`OperationFailed`) for `TestChildObject.TestInt` on this provider. That is
documented as parity with the deployed ASB provider, not a successful item-level
cleanup status.
The pure .NET 10 client also handles multi-item register/read bodies. A two-tag
probe registered and read `TestChildObject.TestInt` plus
`TestMachine_001.TestHistoryValue` in single requests; both returned per-item
success and decoded as ASB `TypeInt32`.
For write status callbacks, the public event is tied to the non-length-prefixed
5-byte operation-status body `00 00 50 80 00`. Length-prefixed completion-only
bodies are lower-level NMX status frames and did not produce public
`OnWriteComplete` events in captures `089`, `091`, `092`, or `093`, even when
the completion byte was `0x00`.
## Data types
`MxDataType`:
```text
Unknown = -1
NoData = 0
Boolean = 1
Integer = 2
Float = 3
Double = 4
String = 5
Time = 6
ElapsedTime = 7
ReferenceType = 8
StatusType = 9
Enum = 10
SecurityClassificationEnum = 11
DataQualityType = 12
QualifiedEnum = 13
QualifiedStruct = 14
InternationalizedString = 15
BigString = 16
END = 17
```
Live GR inventory on this node found deployed/configured instances of all core
scalar types plus `ElapsedTime` and `InternationalizedString`. Target references
captured for the two non-core types:
- `TestMachine_001.TestAlarm001.Alarm.TimeDeadband`: `ElapsedTime`, observed
subscription callback wire kind `0x07` with four-byte zero payload in
`captures\063-frida-subscribe-elapsed-time-deadband`.
- `TestChildObject.ShortDesc`: `InternationalizedString`, observed callback
normalizes the empty value to string wire kind `0x05` with compact payload
`04 00 00 00` in `captures\062-frida-subscribe-intl-shortdesc`.
- Non-empty GR defaults exist at `DevPlatform._EngUnitsPercent` and
`DevAppEngine.Scheduler._EngUnitsMB`; captures `064` and `065` resolved and
advised those items but did not emit a value callback during the capture
window.
Write projection for these non-core types is caller-variant based in the
observed MXAccess path. Capture `095` wrote an `Int32` to `ElapsedTime` and
emitted integer wire kind `0x02`; capture `096` wrote a `string` to
`InternationalizedString` and emitted string wire kind `0x05`. The managed
library mirrors those projections for write attempts while keeping the data
types out of generic `TryGetValueKind` classification.
`MxStatus` and `MXSTATUS_PROXY` are identical sequential structs with 4-byte
packing:
```csharp
public short success;
public MxStatusCategory category;
public MxStatusSource detectedBy;
public short detail;
```
`MxStatusCategory`:
```text
Unknown = -1
Ok = 0
Pending = 1
Warning = 2
CommunicationError = 3
ConfigurationError = 4
OperationalError = 5
SecurityError = 6
SoftwareError = 7
OtherError = 8
```
`MxStatusSource`:
```text
Unknown = -1
RequestingLmx = 0
RespondingLmx = 1
RequestingNmx = 2
RespondingNmx = 3
RequestingAutomationObject = 4
RespondingAutomationObject = 5
```
## Status detail text
`C:\Program Files (x86)\Common Files\ArchestrA\Framework\Bin\Lmx.aaDCT`
contains localized text for common status detail codes. The .NET 10 managed
model now includes the installed English entries:
| Detail | Text |
| ---: | --- |
| 16 | Request timed out |
| 17 | Platform communication error |
| 18 | Invalid platform ID |
| 19 | Invalid engine ID |
| 20 | Engine communication error |
| 21 | Invalid reference |
| 22 | No Galaxy Repository |
| 23 | Invalid object ID |
| 24 | Object signature mismatch |
| 25 | Invalid primitive ID |
| 26 | Invalid attribute ID |
| 27 | Invalid property ID |
| 28 | Index out of range |
| 29 | Data out of range |
| 30 | Incorrect data type |
| 31 | Attribute not readable |
| 32 | Attribute not writeable |
| 33 | Write access denied |
| 34 | Unknown error |
| 35 | detected by |
| 36 | Wrong data type |
| 37 | Wrong number of dimensions |
| 38 | Invalid index |
| 39 | Index out of order |
| 40 | Dimension does not exist |
| 41 | Conversion not supported |
| 42 | Unable to convert string |
| 43 | Overflow |
| 44 | Attribute signature mismatch |
| 45 | Resolving local portion of reference |
| 46 | Resolving global portion of reference |
| 47 | Nmx version mismatch |
| 48 | Nmx command not valid |
| 49 | Lmx version mismatch |
| 50 | Lmx command not valid |
| 51 | However, the object could not be put On Scan - Permission to modify "Operate" attributes is required |
| 52 | Unable to resolve reference for 'set' request because Galaxy Repository is busy performing a 'Deploy/Undeploy' operation |
| 53 | Too many outstanding pending requests to engine |
| 54 | Object Initializing |
| 55 | Engine Initializing |
| 56 | Secured Write |
| 57 | Verified Write |
| 58 | No Alarm Ack Privilege |
| 59 | Alarm Acked Already |
| 60 | User did not have the necessary permissions to write |
| 61 | Verifier did not have the necessary permissions to verify |
| 541 | Conversion to intended data type is not supported |
| 542 | Unable to convert the input string to intended data type |
| 8017 | Object must be offscan to modify attributes that have an MxSecurityConfigure security classification |
+397
View File
@@ -0,0 +1,397 @@
# MXAccess reverse-engineering analysis
## Executive summary
The primary `ArchestrA.MXAccess.dll` is not a protocol implementation. It is a
.NET primary interop assembly generated from `LMXPROXYLib`. All useful methods
on `LMXProxyServerClass` are `extern` COM calls. The actual implementation is
native, 32-bit, and registered as an in-process COM server:
```text
net48 x86 caller
-> ArchestrA.MXAccess.dll
-> COM CLSID {C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}
-> C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
-> C:\Program Files (x86)\Common Files\ArchestrA\Framework\Bin\Lmx.dll
-> C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll
-> C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe
-> runtime engines over NMX
```
That is why the current OPC UA interface is constrained to .NET Framework and
x86. The binding itself is not forcing .NET 4.8; the 32-bit COM/native stack is.
The cleanest modern replacement is not "port the interop assembly"; it is either:
1. keep a 32-bit sidecar process and expose a modern IPC API to .NET 10/Rust, or
2. implement the lower LMX/NMX client protocol directly from the native type
libraries plus runtime traces.
Option 2 is possible but not complete from static decompilation alone because
the proprietary message bodies are assembled inside native code, not in
`ArchestrA.MXAccess.dll`.
## Evidence
### Managed DLL
`ArchestrA.MXAccess.dll`:
- File path: `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`
- Assembly: `ArchestrA.MxAccess, Version=3.2.0.0, PublicKeyToken=23106a86e706d0ae`
- File description/product: `Assembly imported from type library 'LMXPROXYLib'.`
- Decompilation contains only COM-imported interfaces, structs, enums, delegates,
and event sink helpers.
- `LMXProxyServerClass` methods are all `runtime managed internalcall`, meaning
the CLR dispatches them through COM; there is no managed implementation body to port.
### COM registration
`LMXProxy.LMXProxyServer` is registered only in the 32-bit COM registry view:
```text
HKCR\Wow6432Node\CLSID\{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}
(default) = LMXProxyServer Class
InprocServer32 = C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
ThreadingModel = Apartment
ProgID = LMXProxy.LMXProxyServer.1
VersionIndependentProgID = LMXProxy.LMXProxyServer
TypeLib = {C36ECF28-3EF3-4528-9843-81D7FDC86328}
```
Type libraries:
```text
{77E896EC-B3E3-4DE4-B96F-F32570164211}\3.2
LMXProxy 3.2 Type Library
C:\Program Files (x86)\ArchestrA\Framework\Bin\MXAccess32.tlb
{C36ECF28-3EF3-4528-9843-81D7FDC86328}\1.0
LMXProxy 1.0 Type Library
C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
{C36ECF28-3EF3-4528-9843-81D7FDC86328}\2.0
LMXProxy 2.0 Type Library
C:\Program Files (x86)\ArchestrA\Framework\Bin\MXAccess20.tlb
```
### Native binaries
| Binary | Role inferred from registration/imports/strings | Architecture |
| --- | --- | --- |
| `LmxProxy.dll` | Public MXAccess COM wrapper. Implements `LMXProxy.LMXProxyServer`. Imports licensing wrapper and COM/OLE Automation APIs. | PE32 x86 |
| `Lmx.dll` | Main local message exchange implementation. Exposes many COM objects including `IDataClient`, `IDataConsumer`, `Nmx`, bootstrap, runtime object, and data variant surfaces. | PE32 x86 |
| `NmxAdptr.dll` | In-process adapter over NMX service. Imports Winsock and exposes `WonderWare.Nmx.CNmxAdapter`. | PE32 x86 |
| `NmxSvc.exe` | Out-of-process NMX service. Registered as `NmxSvc.NmxService`; listens on TCP/UDP port `5026` on this machine. | PE32 x86 |
| `NmxSvcps.dll` | MIDL proxy/stub for NMX service COM interfaces. | PE32 x86 |
`LmxProxy.dll` exports only standard COM DLL exports:
```text
DllCanUnloadNow
DllGetClassObject
DllRegisterServer
DllUnregisterServer
```
This means there is no flat C ABI to P/Invoke from modern .NET or Rust.
`LmxProxy.dll` imports `LicAPINativeWrapper.dll`, including:
```text
CreateClientConnection
AddLicenseRequestInfo
AcquireLicense
GetLicenseAcquisitionError
ReleaseLicense
GetDeviceIdentity
```
Any native replacement has to account for AVEVA licensing behavior, either by
using a supported client surface or by keeping a licensed sidecar.
## Runtime model inferred from MXAccess
The current high-level API is handle based:
1. `Register(clientName)` returns an `hLMXServerHandle`.
2. `AddItem(handle, fullReference)` resolves a Galaxy/runtime attribute and
returns an item handle.
3. `Advise` or `AdviseSupervisory` subscribes to updates.
4. Updates arrive through COM connection-point events:
`OnDataChange`, `OnWriteComplete`, and `OperationComplete`.
5. `Write`/`Write2` submit writes and completion is reported asynchronously.
6. `UnAdvise`, `RemoveItem`, and `Unregister` clean up.
The existing OPC UA host uses the canonical pattern correctly:
- All COM work runs on one STA thread.
- Reads are implemented as transient subscribe -> first `OnDataChange` -> unsubscribe because MXAccess exposes no direct synchronous read.
- Writes are submitted with `Write` and then matched with `OnWriteComplete`.
- Subscriptions use `AdviseSupervisory`.
## Lower LMX data-client surface
`Lmx.dll` exposes a richer COM interface named `IDataClient`. It is still x86 COM,
but it is closer to the underlying data model than `LMXProxyServer`:
```csharp
void Initialize(string namespaceName);
IntPtr Connect(string endPointUri, ulong timeout, ref _IUserToken userToken, out uint clientId);
IArchestrAResult[] Connect2(...);
long CreateSubscription2(...);
IArchestrAResult[] RegisterItems2(IItemIdentity2[] items, ...);
IArchestrAResult[] AddMonitoredItems2(long subscriptionId, IMonitoredItem2[] items, ...);
IArchestrAResult[] Publish2(long subscriptionId, out DataChangeUpdate[] updates, out uint resultCount);
IArchestrAResult[] Read2(IItemIdentity2[] items, out DataChangeUpdate[] updates, out uint updatesCount);
IArchestrAResult[] Write2(WriteRequest2[] writes, ...);
```
Important structs:
```csharp
struct IItemIdentity2 {
ushort ReferenceType;
ushort type;
string ContextName;
string Name;
ulong id;
}
struct IMonitoredItem2 {
byte Active;
ushort ReferenceType;
ushort type;
string ContextName;
string Name;
ulong id;
ulong SampleInterval;
ulong TimeDeadband;
}
struct DataChangeUpdate {
ulong ItemId;
int StatusCode;
uint HighDateTime;
uint LowDateTime;
int quality;
IDataVariant value;
}
struct IDataVariant {
ushort type;
int length;
byte[] Payload;
}
```
This surface suggests AVEVA has a newer OPC UA-like internal data client in the
LMX layer. It may be a better reverse-engineering target than `LMXProxyServer`,
but it is not registered as a 64-bit component on this machine. It still goes
through the x86 native stack.
## NMX service surface
`NmxSvc.exe` is registered as an out-of-process COM local server:
```text
CLSID {AE24BD51-2E80-44CC-905B-E5446C942BEB}
LocalServer32 = "C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe"
ProgID = NmxSvc.NmxService.1
```
Key `INmxService2` methods:
```csharp
void RegisterEngine2(int localEngineId, string engineName, int version, INmxSvcCallback callback);
void UnRegisterEngine(int localEngineId);
void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte msgBody);
void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
void RemoveSubscriberEngine(...);
void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
void GetPartnerVersion(int galaxyId, int platformId, int engineId, out int version);
```
`NmxAdptr.dll` exposes `WonderWare.Nmx.CNmxAdapter` as an in-process COM adapter:
```text
CLSID {42DB0511-28BE-11D3-80C0-00104B5F96A7}
InprocServer32 = C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll
ProgID = WonderWare.Nmx.CNmxAdapter.1
ThreadingModel = Apartment
```
Key `INmx4` methods:
```csharp
void Initialize2(int platformId, int engineId, int version, int queueExtent);
void InitializeAnonymous2(out int platformId, out int engineId, int version);
void PutRequest2(int galaxyId, int platformId, int engineId, byte priority, byte type, int size, ref byte data, out int requestHandle);
void GetResponse2(byte type, out int responseCode, out int requestHandle, out int size, IntPtr data);
void PutMessageNoReply(...);
void GetPartnerVersion(...);
```
Static strings identify NMX message categories:
```text
NMXMTYPE_CONNECT_INFO
NMXMTYPE_CONNECT_INFO_REQ
NMXMTYPE_ENGINE_DATA
NMXMTYPE_GET_HEARTBEAT_RATE
NMXMTYPE_PLATFORM_HEARTBEAT
NMXMTYPE_SET_HEARTBEAT_RATE
NMXMTYPE_VERSION_ERROR
NMXMTYPE_HEARTBEAT_DISCONNECT_REQ
```
On this machine, the running `NmxSvc.exe` process is listening on:
```text
TCP 10.100.0.48:5026
UDP 10.100.0.48:5026
```
That gives a network tracing target for the direct-native path.
## Why direct x64 COM is blocked
There are two different issues:
1. `ArchestrA.MXAccess.dll` is a .NET Framework-era interop assembly. Modern
.NET can consume many COM interfaces, but the generated event provider and
TLB-imported types are built for the old COM interop shape.
2. The actual COM class is an x86 in-process DLL. A 64-bit process cannot load
`LmxProxy.dll`, regardless of whether the caller is .NET Framework, .NET 10,
or Rust.
An out-of-process COM local server can cross bitness, but `LMXProxyServer` is
not registered as a local server. It is an in-proc DLL.
## Implementation options
### Option A: Keep and formalize the 32-bit sidecar
This is the shortest reliable path.
Build or keep a small x86 process that owns:
- COM apartment initialization.
- `LMXProxy.LMXProxyServer` lifetime.
- register/add/advise/write/unadvise cleanup.
- reconnect and subscription replay.
Expose a stable IPC API to the modern service:
- named pipes for same-machine .NET/Rust services,
- gRPC over named pipes or loopback TCP if cross-language tooling matters,
- protobuf messages for `Read`, `Write`, `Subscribe`, `Unsubscribe`, and status events.
This is effectively what the existing `OtOpcUaGalaxyHost` / proxy work has moved
toward. It removes the OPC UA server's net48/x86 constraint without attempting
to clone AVEVA's internal protocol.
### Option B: Use lower LMX COM interfaces from an x86 sidecar
Instead of the high-level `LMXProxyServer`, use `Lmx.dll`'s `IDataClient` /
`IDataConsumer` APIs inside the sidecar. Potential benefits:
- direct `Read2` API rather than transient subscribe for reads,
- batch register/read/write/monitor operations,
- explicit `IDataVariant` payloads,
- closer alignment with OPC UA-like status codes.
Risks:
- less documented than MXAccess,
- still x86 COM,
- endpoint URI, namespace, token setup, and licensing behavior need live probing.
### Option C: Direct NMX protocol client in Rust or .NET
This is the true native replacement path.
Required work:
1. Discover galaxy/platform/engine ids for target attributes.
2. Reconstruct NMX message envelope layout used by `PutRequest2` and
`NmxSvc.TransferData`.
3. Reconstruct LMX item registration, subscription, write, and completion
message bodies.
4. Reproduce heartbeat/version negotiation.
5. Reproduce security/licensing behavior or find a supported token flow.
6. Validate behavior across platform stopped/off-scan, engine restart, deploy,
security-denied write, and repository-busy cases.
Static type-library analysis gives method names and high-level message routes,
but not the binary message schemas. Those schemas are assembled in native code.
The next step for this option is live tracing.
## Proposed tracing plan
1. Build a minimal x86 MXAccess harness that:
- registers as a known client name,
- adds one known good tag and one bad tag,
- subscribes with `AdviseSupervisory`,
- writes a toggled value,
- unregisters cleanly.
2. Capture process activity:
- Process Monitor filters for `LmxProxy.dll`, `Lmx.dll`, `NmxAdptr.dll`,
`NmxSvc.exe`.
- ETW or API Monitor for COM method calls if available.
- Wireshark or `netsh trace` on TCP/UDP `5026`.
3. Correlate high-level actions to low-level packets:
- `Register`
- `AddItem`
- `AdviseSupervisory`
- first data change
- `Write`
- `OnWriteComplete`
- `UnAdvise` / `RemoveItem`
4. Repeat against:
- local object attribute,
- remote platform attribute,
- stopped engine,
- security-denied write,
- secured/verified write.
5. Use `NmxAdptr.INmx4` as a stepping stone:
- call `InitializeAnonymous2`,
- issue controlled `PutRequest2` from an x86 harness,
- compare buffers and responses with MXAccess traffic.
## Current recommendation
For production, do not block the OPC UA server on a full native NMX clone. The
low-risk path is:
1. keep the x86 MXAccess host as the vendor-bound adapter,
2. make the main OPC UA interface .NET 10 or Rust,
3. communicate through a narrow, well-tested IPC contract,
4. continue reverse-engineering `IDataClient` and NMX in parallel as an R&D path.
The direct native path may still be worth pursuing, but it should be treated as
a protocol-reconstruction project with packet captures and failure-mode testing,
not as a decompile-only port.
## Artifacts produced
Generated files in this project folder:
```text
analysis/decompiled-mxaccess/
analysis/interop/
analysis/decompiled-interop/
```
Installed tools used:
```text
dotnet tool install --global ilspycmd
python pefile
Windows SDK TlbImp.exe
```
+295
View File
@@ -0,0 +1,295 @@
# Managed LMX/NMX capture plan
Goal: build a full managed .NET 10 x64 DLL that talks to AVEVA/Wonderware
LMX/NMX without loading the x86 MXAccess COM/native stack.
Assumption: licensing is not a blocker for this environment. The capture plan
still records licensing and security behavior because the managed client must
match runtime behavior for secured writes, permission failures, and audit paths.
## Success criteria
The managed DLL is viable when it can perform these operations from an x64
.NET 10 process using only managed code:
- discover or configure the local Galaxy/runtime endpoint,
- resolve a Galaxy attribute reference such as `Object.Attribute`,
- read current value, timestamp, quality, and status,
- subscribe and receive initial value plus subsequent data changes,
- write standard values and receive definitive completion status,
- preserve bad-quality/status semantics when a platform or engine stops,
- handle reconnect, heartbeat, runtime restart, and deploy/undeploy churn,
- map common MXAccess errors to deterministic managed status codes.
## Known protocol gaps
Static analysis gave us interfaces and routing, but not enough wire/message
detail. These are the missing pieces to capture:
| Area | Missing detail |
| --- | --- |
| NMX session startup | version negotiation, anonymous engine allocation, heartbeat setup, local/remote ids |
| Endpoint discovery | how local Galaxy/platform/engine ids are chosen or discovered |
| Item resolution | request body for `AddItem`, `AddItem2`, invalid-reference response |
| Subscription | request body for `Advise`, `AdviseSupervisory`, buffered items, initial value behavior |
| Data update | payload format for value, type, quality, timestamp, `MXSTATUS_PROXY` |
| Reads | whether MXAccess implements read as transient subscription only or lower `IDataClient.Read2` messages can be used directly |
| Writes | standard write body, timestamped write, array write, status and completion correlation |
| Security | secured write, verified write, user id/GUID mapping, denied-write status |
| Lifecycle | unregister/unadvise/remove cleanup messages |
| Failure modes | platform down, engine down, bad reference, repository busy, wrong type, permission denied |
## Phase 1: controlled MXAccess harness
Build a tiny x86 harness that uses the primary MXAccess stack exactly as
production does:
```text
ArchestrA.MXAccess.dll -> LMXProxy.LMXProxyServer -> LmxProxy.dll
```
Harness requirements:
- target `net48`, `x86`,
- single STA thread,
- deterministic client name, for example `MxProtoTraceHarness`,
- configurable tag list and write values,
- timestamps every high-level API call,
- logs:
- `Register` handle,
- every `AddItem` item handle,
- every `AdviseSupervisory`,
- every `OnDataChange`,
- every `Write`,
- every `OnWriteComplete`,
- every status array,
- cleanup calls.
Initial scenarios:
1. Register/unregister only.
2. Add/remove one known-good read-only item.
3. Subscribe/unsubscribe one known-good item.
4. One-shot read pattern: add, advise, wait first callback, unadvise, remove.
5. Write one boolean or integer setpoint and capture completion.
6. Add invalid item reference.
7. Subscribe to a stopped/unavailable runtime host probe if available.
Artifacts:
- harness source,
- timestamped harness log,
- Process Monitor trace,
- network trace,
- optional API Monitor trace.
Exit criteria:
- one repeatable capture set where every high-level harness operation can be
correlated to lower process/network activity.
## Phase 2: process and network capture
Capture layers:
1. Process Monitor:
- processes: harness, `NmxSvc.exe`, `aaBootstrap`, relevant AppEngine/Platform processes,
- operations: registry, file, process/thread, TCP/UDP where available,
- objective: confirm DLLs, config, registry keys, and runtime dependencies touched per operation.
2. Network:
- filter: host IP plus TCP/UDP port `5026`,
- tools: Wireshark and/or `netsh trace`,
- objective: recover packet framing and message timing.
3. COM/API call tracing:
- target `LmxProxy.dll`, `Lmx.dll`, `NmxAdptr.dll`, `NmxSvc.exe`,
- APIs of interest:
- COM object creation,
- `NmxSvc.NmxService.TransferData`,
- `NmxAdptr.INmx4.PutRequest2`,
- `NmxAdptr.INmx4.GetResponse2`,
- `IDataClient.*` if used,
- Winsock `sendto`, `recvfrom`, TCP send/recv.
Capture naming:
```text
captures/
001-register/
002-additem-good/
003-subscribe-good/
004-write-good/
005-additem-invalid/
```
Each folder should contain:
```text
harness.log
procmon.pml
network.pcapng
notes.md
```
Exit criteria:
- identify which layer carries the actual item-resolution, subscribe, update,
and write messages.
- know whether the best implementation target is:
- direct NMX socket protocol,
- local COM `NmxSvc` automation,
- lower `Lmx.dll` `IDataClient` behavior replicated over sockets.
## Phase 3: schema reconstruction
Start with messages that have simple cause/effect:
1. `Register` / startup heartbeat.
2. `AddItem` good versus invalid reference.
3. `AdviseSupervisory` initial value.
4. `Write` and `OnWriteComplete`.
For each message:
- identify envelope:
- magic/version,
- message type,
- source galaxy/platform/engine id,
- destination galaxy/platform/engine id,
- request handle/correlation id,
- payload length,
- checksum if any.
- identify payload:
- item reference string encoding,
- context string,
- item handle/id,
- Mx data type,
- quality,
- FILETIME timestamp,
- status category/source/detail.
Use differential captures:
- same operation, different tag name,
- same tag, different value,
- valid tag versus invalid tag,
- boolean versus integer versus string,
- normal write versus denied write.
Exit criteria:
- documented binary structs for the minimum viable read/subscribe/write path,
- sample parser that can decode captured traffic into structured JSON.
## Phase 4: managed transport prototype
Build a new .NET 10 x64 prototype library:
```text
src/ManagedLmxNmx/
LmxNmxClient.cs
NmxTransport.cs
NmxFrame.cs
LmxMessages.cs
MxValueCodec.cs
MxStatus.cs
```
Prototype scope:
- connect to local NMX endpoint,
- perform startup/version/heartbeat,
- resolve one item,
- subscribe to one item,
- decode updates,
- write one item,
- disconnect cleanly.
Hard rules:
- no COM,
- no native P/Invoke except optional OS primitives already wrapped by .NET,
- no x86 process dependency,
- deterministic timeouts and cancellation,
- all binary parsers bounds-checked.
Exit criteria:
- x64 test app receives the same value/quality/timestamp as MXAccess for a known tag.
- x64 test app can write and observe `OnWriteComplete` equivalent status.
## Phase 5: parity suite
Create side-by-side tests:
```text
MXAccess x86 harness result == Managed x64 result
```
Parity matrix:
| Scenario | Expected |
| --- | --- |
| Good boolean read | same value, quality, timestamp within tolerance |
| Good numeric read | same value/type/quality |
| Good string read | same value/type/quality |
| Invalid reference | same category/source/detail |
| Subscribe initial callback | same initial value behavior |
| Subscribe change callback | same change behavior |
| Write allowed | same completion success |
| Write wrong type | same failure detail |
| Write denied | same security failure detail |
| Platform stopped | same bad quality/status behavior |
| Engine restart | reconnect and resubscribe |
| Deploy/undeploy busy | same busy status |
Exit criteria:
- managed client passes parity for the minimum production tag classes,
- documented unsupported cases are explicit.
## Phase 6: production hardening
Before replacing the sidecar:
- fuzz message parsers with captured and mutated frames,
- soak-test subscriptions with production-scale tag counts,
- run AppEngine/Platform stop-start tests,
- verify no unbounded queues,
- verify reconnect backoff,
- verify audit/security behavior for writes,
- expose metrics:
- connected state,
- heartbeat age,
- subscription count,
- pending request count,
- reconnect count,
- bad frame count.
## Immediate next work items
Completed in the 2026-04-25 run:
1. Created the x86 MXAccess trace harness at `src\MxTraceHarness`.
2. Queried the Galaxy repository for safe candidate tags using
`analysis\sql\select_capture_tags.sql`.
3. Captured register, add/remove, subscribe, invalid reference, array naming,
and advised write scenarios under `captures\`.
4. Converted `netsh.etl` traces to `network.pcapng` with `etl2pcapng`.
5. Installed Wireshark 4.6.4 and attempted Npcap 1.87 installation.
Current blocker:
- The local NMX payload is on a loopback path (`::1`) that `netsh trace` and
`pktmon` did not expose as pcap payload in this session. `dumpcap` cannot
capture until Npcap installs successfully.
Next work items:
1. Complete an elevated/interactive Npcap install or use API Monitor.
2. Recapture scalar subscribe, array subscribe, invalid subscribe, and advised
write on the NMX loopback path.
3. Write the first decoder for loopback NMX frame boundaries and timestamps.
4. Decide whether direct socket protocol or lower `IDataClient` behavior is the
better implementation target after loopback payloads are available.
+426
View File
@@ -0,0 +1,426 @@
# MxNativeSession API
`MxNativeSession` is the first high-level .NET 10 x64 facade over the managed
NMX protocol work in this repository. It is intended to be the consumer-facing
entry point while the lower layers remain available for protocol tests and
diagnostics.
## Runtime prerequisites
- AVEVA System Platform/NMX installed locally so `NmxSvc.NmxService` can be
activated.
- The Galaxy Repository SQL database available to the current Windows session.
- Managed NTLM runtime credentials supplied through the existing environment
variable path used by `ManagedNtlmClientContext`.
- The x64 callback COM registration/type-library setup already documented in
`DotNet10-Native-Library-Plan.md`.
The session facade does not store credentials. It relies on the process
environment and the already-authenticated Windows/SQL context.
## Basic write
```csharp
using MxNativeClient;
using var session = MxNativeSession.Open(new MxNativeClientOptions
{
LocalEngineId = 0x7fee,
EngineName = "MxNativeClient.Sample",
// Optional. Leave null unless a deployment-specific heartbeat cadence is known.
HeartbeatTicksPerBeat = null,
});
await session.WriteAsync("TestChildObject.TestInt", 123);
await session.Write2Async(
"TestChildObject.TestInt",
124,
DateTime.UtcNow);
```
`HeartbeatTicksPerBeat` and `HeartbeatMaxMissedTicks` expose
`INmxService2.SetHeartbeatSendInterval`. A .NET 10 x64 probe validated
`SetHeartbeatSendInterval(5, 3)` against local NmxSvc. The setting remains
opt-in because the exact MXAccess heartbeat cadence has not been captured on
this VM; leaving `HeartbeatTicksPerBeat` null preserves the previously
validated connection behavior.
## Recovery
`MxNativeSession.RecoverConnection()` explicitly rebuilds the managed NMX
service client, re-registers the local engine and callback, reapplies optional
heartbeat settings, reconnects known publisher engines, and replays current
normal or buffered subscriptions with their existing correlation IDs. The
compatibility facade exposes the same operation as
`MxNativeCompatibilityServer.RecoverConnection(serverHandle)`.
`RecoverConnectionAsync(policy, cancellationToken)` adds an explicit retry loop
around that primitive. `MxNativeRecoveryPolicy.MaxAttempts` defaults to `1`;
`Delay` defaults to zero. The compatibility facade exposes the same retrying
operation as `RecoverConnectionAsync(serverHandle, policy, cancellationToken)`.
Recovery progress is observable:
- `RecoveryAttemptStarted` fires before each reconnect/replay attempt.
- `RecoveryAttemptFailed` fires after a recoverable attempt failure and reports
the exception plus whether the retry loop will continue.
- `RecoveryCompleted` fires after the replacement service has been registered,
publisher endpoints have been reconnected, subscriptions have been replayed,
and the session has swapped to the recovered service.
The compatibility facade exposes the same events with the server handle added
to each event payload. A live .NET 10 x64 probe with two allowed attempts and a
100 ms delay observed one started event, zero failed events, one completed
event, preserved the existing subscription count, and wrote through the
recovered session.
Callbacks are passed through during the recovery window instead of being
suppressed or buffered. `MxNativeCallbackEvent`,
`MxNativeOperationStatusEvent`, `MxNativeReferenceRegistrationEvent`, and
`MxNativeUnparsedCallbackEvent` include `IsDuringRecovery` so callers can
separate replay-window events from steady-state events. Compatibility
`DataChanged`, `BufferedDataChanged`, and `WriteCompleted` payloads carry the
same marker.
`MxNativeClient.Probe --probe-session-recover-multi` subscribes to multiple
tags, runs explicit recovery, and reports total callbacks plus
`IsDuringRecovery` counts for data, operation-status, reference-registration,
and unparsed callback families. The first live run replayed four subscriptions
and preserved all four, with zero callback events marked as recovery-window
events on this VM.
Adding `--recover-concurrent-writes` starts a separate writer session during
the same recovery window. A live run wrote `TestChildObject.TestInt` values
`330`-`334`, preserved all four subscriptions, and observed two data callbacks
with `IsDuringRecovery=true`. This confirms that callbacks are not quiesced by
the library during recovery; consumers that require strict post-recovery
ordering should ignore or queue marked events at their boundary.
Automatic background recovery is intentionally not enabled yet. Retrying writes
inside normal write calls can duplicate writes, so callers should choose where
recovery belongs in their own operation model. A .NET 10 x64 probe validated the
current explicit path by subscribing to `TestChildObject.TestInt`, recovering
the connection, preserving the subscription count, and writing through the
recovered session.
`WriteSecured2Async` is implemented for the observed boolean secured/verified
payload shape:
```csharp
await session.WriteSecured2Async(
"TestMachine_001.ProtectedValue",
true,
DateTime.Now,
currentUserId: 1,
verifierUserId: 0);
```
The encoder emits native command `0x38` and has been live validated from .NET
10 x64 against the secured and verified boolean tags deployed on this node, as
well as authenticated bool, int, float, double, string, datetime, and
scalar-array calls against `TestChildObject` attributes. The handle-based
compatibility facade also supports `AuthenticateUser` followed by
`WriteSecured2`, and intentionally does not synthesize `OnWriteComplete` for
this path because the successful native captures did not show that event.
`WriteSecuredAsync` remains unsupported because native MXAccess still returns
`0x80004021` before emitting a value-bearing body in every captured scenario.
Additional native captures beyond bool and int would improve fixture coverage,
but the managed encoder is now generic over the timestamped write-body support.
## Browse
```csharp
IReadOnlyList<GalaxyTagMetadata> tags =
await session.BrowseAsync("TestChildObject", "Test%", maxRows: 25);
```
`BrowseAsync` uses Galaxy Repository metadata directly. It does not call the
x86 LMX resolver and returns the same metadata needed by the managed handle and
wire encoders.
`ResolveAsync` accepts full references in the observed MXAccess forms:
`Object.Attribute`, `Object.Primitive.Attribute`, and
`Object.Primitive.Dotted.Attribute` for primitive attributes such as
`TestMachine_001.TestAlarm001.Alarm.TimeDeadband`. It also recognizes the
captured literal property form `Object.Attribute.property(buffer)`, resolving it
to the base attribute handle with property id `0x32`.
Each `GalaxyTagMetadata` exposes `IsSupportedValueKind` and
`TryGetValueKind(out MxValueKind valueKind)`. Use those before write/read
projection when browsing broad GR metadata. The current wire codec supports the
live OPC-UA-critical GR types `Boolean`, `Integer`, `Float`, `Double`, `String`,
`Time`, and array forms captured so far. Live GR inspection also found
`ElapsedTime` and `InternationalizedString`. Subscribe/read callback decoding now
handles the observed `ElapsedTime` wire kind `0x07` as `TimeSpan` and the compact
empty `InternationalizedString`/string payload form `0x05 04 00 00 00` as
`string.Empty`. They are still reported as unsupported by `TryGetValueKind`
because they are not core one-to-one data kinds, but write projection now follows
captured MXAccess caller-variant behavior: `ElapsedTime` values supplied as
`TimeSpan` or integer project to integer milliseconds/wire kind `0x02`, and
`InternationalizedString` values project to normal string wire kind `0x05`.
`WriteAsync` and `Write2Async` resolve the tag from Galaxy Repository metadata,
build the managed `MxReferenceHandle`, encode the NMX write body, wrap it in
the `TransferData` envelope, and call `INmxService2::TransferData` through the
managed DCOM path.
## Subscribe
```csharp
using MxNativeClient;
using var session = MxNativeSession.Open();
session.CallbackReceived += (_, evt) =>
{
Console.WriteLine(
$"{evt.Record.TimestampUtc:O} status={evt.Status.Category} quality=0x{evt.Record.Quality:X4} value={evt.Record.Value}");
};
session.OperationStatusReceived += (_, evt) =>
{
Console.WriteLine(
$"operation_status=0x{evt.Message.StatusCode:X4} status={evt.Message.Status.Category}");
};
MxNativeSubscription sub = await session.SubscribeAsync("TestChildObject.TestInt");
await Task.Delay(TimeSpan.FromSeconds(10));
session.Unsubscribe(sub.CorrelationId);
```
## Read
```csharp
object? value = await session.ReadAsync(
"TestChildObject.TestInt",
timeout: TimeSpan.FromSeconds(10));
```
`ReadAsync` is implemented as a transient subscription read. It subscribes,
waits for the first typed value callback matching that item correlation, then
unsubscribes.
`SubscribeAsync` performs the managed equivalent of the observed MXAccess path:
1. Resolve tag metadata from the Galaxy Repository.
2. Connect the local engine to the tag's owning engine.
3. Add the local engine as a subscriber.
4. Send the generated item-control `0x1f` body using the native value handle.
5. Decode incoming `INmxSvcCallback` bodies with `NmxSubscriptionMessage`.
Capture `099-frida-plain-advise-testint` showed public `Advise` and the earlier
`AdviseSupervisory` scalar path using the same `0x1f` body shape for
`TestChildObject.TestInt`, so the compatibility wrapper maps both methods onto
this subscription path.
`CallbackReceived` is raised for typed `0x32` subscription-status and `0x33`
data-update records. Each callback exposes the raw NMX record fields plus a
first-pass `MxStatus` projection. `MxStatus.DetailText` returns the installed
English `Lmx.aaDCT` detail text for known LMX/NMX status details, including
reference, conversion, security, alarm, initializing, and secured/verified
write conditions. `OperationStatusReceived`
is raised for both observed operation-status frame forms. The non-length-prefixed
5-byte status-word form with status word `0x8050` and completion byte `0x00`
maps to `MxStatus.WriteCompleteOk` and is the captured public
`OnWriteComplete` success notification. Length-prefixed completion-only frames
are decoded separately and expose `NmxOperationStatusFormat.CompletionOnly`.
Captures `089`, `092`, and `093` produced completion byte `0x41` after
wrong-type string writes, and capture `091` produced completion byte `0x00`
after double-to-int coercion; MXAccess did not raise `OnWriteComplete` for
either completion-only form.
The managed x64 path now has a proven value-bearing subscription fixture:
`SubscribeAsync("TestChildObject.ShortDesc")` receives the same compact empty
string callback observed from x86 MXAccess (`0x32`, wire kind `0x05`, quality
`0x00C0`). The key handle detail is that value references use property id `10`;
GR `mx_attribute_category` is metadata and is not the NMX value-handle property
field.
String-array callback handling is intentionally conservative. Captures `100`
and `101` showed `0x45` string-array callback records whose buffers stopped
inside the final element, and MXAccess did not raise a public data-change event
for those runs. The decoder does not fabricate a value for malformed array
payloads; callers should treat a `null` value with a known array wire kind as an
incomplete callback until a complete/public string-array callback is captured.
`ReferenceRegistrationReceived` is raised for decoded `0x11`
reference-registration result frames. Those frames are emitted after the `0x10`
normal/buffered registration request and include the item handle, correlation
ID, reference text, context, and MX data type.
`UnparsedCallbackReceived` is a diagnostic event for callback bodies that are
not yet part of the high-level API surface. It is currently used to expose
frames such as the 92-byte operation/status-shaped body seen before scalar
subscription status callbacks, rather than silently swallowing unknown protocol
bodies during reverse engineering.
## Probe commands
The probe now has facade-level entry points:
```powershell
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-write --tag=TestChildObject.TestInt --value=123 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-read --tag=TestChildObject.TestInt --read-timeout-seconds=10 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-subscribe --tag=TestChildObject.TestInt --subscribe-hold-seconds=5 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-subscribe --tag=TestChildObject.ShortDesc --subscribe-hold-seconds=8 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-subscribe --tag=TestChildObject.ShortDesc --subscribe-hold-seconds=8 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-subscribe-write --tag=TestChildObject.TestInt --value=793 --subscribe-hold-seconds=8 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-subscribe --tag=NoSuchObject_999.NoSuchAttr --subscribe-hold-seconds=2 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-subscribe-multi --tag=TestChildObject.ShortDesc --tag=TestChildObject.TestInt --tag=NoSuchObject_999.NoSuchAttr --subscribe-hold-seconds=4 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-write-multi --subscribe-hold-seconds=4 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-write-unadvised --tag=TestChildObject.TestInt --value=99 --objref-only
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-managed-subscribe --tag=TestChildObject.TestInt --subscribe-hold-seconds=5 --send-observed-preadvise-metadata --objref-only
```
These commands require the same runtime managed NTLM environment as the lower
level managed probes.
## MXAccess-style compatibility wrapper
`MxNativeCompatibilityServer` provides an MXAccess-like handle model for code
that expects integer server and item handles:
```csharp
using MxNativeClient;
using var mx = new MxNativeCompatibilityServer();
mx.DataChanged += (_, evt) =>
{
Console.WriteLine(
$"server={evt.ServerHandle} item={evt.ItemHandle} value={evt.Value} quality=0x{evt.Quality:X4}");
};
mx.WriteCompleted += (_, evt) =>
{
Console.WriteLine(
$"server={evt.ServerHandle} item={evt.ItemHandle} status={evt.Statuses[0].Category}");
};
int server = mx.Register("OpcUaBridge.Native");
int item = mx.AddItem(server, "TestChildObject.TestInt");
mx.AdviseSupervisory(server, item);
mx.Write(server, item, 123);
mx.UnAdvise(server, item);
mx.RemoveItem(server, item);
mx.Unregister(server);
```
The compatibility wrapper maps `Register`, `Unregister`, `AddItem`,
`RemoveItem`, `Advise`, `AdviseSupervisory`, `UnAdvise`, `Write`, and `Write2`
onto `MxNativeSession`. `Suspend` and `Activate` mirror observed MXAccess local
behavior for advised items: unadvised calls throw, `Suspend` returns
pending/requesting-LMX status, and `Activate` returns ok/requesting-LMX status.
After `RemoveItem`, the wrapper mirrors native x86 stale-item behavior by
returning `ArgumentException` with `HResult=0x80070057` for advise, unadvise,
write, write2, suspend, activate, and repeated remove attempts.
Invalid server handles and cross-server item handles use the same
`ArgumentException`/`0x80070057` shape observed from native MXAccess.
Literal `property(buffer)` items added with `AddItem` are not treated as
`AddBufferedItem` handles. They follow the captured MXAccess literal-reference
path: add/advise succeeds, normal write returns without a public write-complete,
and no public data-change is promoted on the current VM state.
`AddItem2` resolves the item definition directly first and then retries with the
supplied context. This covers the captured simple form
`AddItem2("TestInt", "TestChildObject")` plus dotted relative forms such as
`AddItem2("Alarm.TimeDeadband", "TestMachine_001.TestAlarm001")` and
`AddItem2("TestInt.property(buffer)", "TestChildObject")`.
`AddBufferedItem` and `SetBufferedUpdateInterval` now cover the decoded public
API path: buffered add creates a handle, the interval is stored as local session
state, and advising the buffered handle sends the observed NMX `0x10`
registration for `itemDefinition.property(buffer)` in the supplied item
context. Headless Ghidra analysis shows native MXAccess routes buffered item
callbacks through the same `OnDataChange` callback method as normal items, then
branches to `_ILMXProxyServerEvents2::OnBufferedDataChange` when the item record
is marked buffered. `MxNativeCompatibilityServer.BufferedDataChanged` mirrors
that separation for any parsed buffered callback and does not promote buffered
items through `DataChanged`. Captures against `TestChildObject.TestInt` and
GR-confirmed historized `TestMachine_001.TestHistoryValue` prove the outbound
context-bearing registration/result bodies, but this VM has not emitted a live
`OnBufferedDataChange` payload. True multi-sample buffered payload decoding
still needs that runtime condition.
`ArchestrAUserToId` follows the observed MXAccess x86
behavior on this node: known user GUIDs and an invalid zero GUID all return `1`.
`AuthenticateUser` follows the same session-local handle model for the observed
dev-node behavior: MXAccess returned `S_OK` and user ID `1` for both
`Administrator` and an invalid user name with an empty password. The managed
compatibility method validates the server handle and user string, returns a
session-local handle, and does not store or compare password material.
`WriteCompleted` is raised only for the captured MXAccess-visible 5-byte
`0x8050/0x00` operation-status frame. Completion-only frames are still available
through `MxNativeSession.OperationStatusReceived`, but the compatibility wrapper
does not convert them into `OnWriteComplete` events because x86 MXAccess did not
fire that event in those captures. Pending write handles are queued per server
session, so concurrent compatibility sessions do not consume each other's
completion callbacks. The mixed `--probe-compatibility-write-multi` path
validates that normal int, internationalized string, literal buffer-property,
and invalid-reference items in one server session do not leak public data-change
or write-complete events across item handles; only the invalid reference emits
the expected configuration-error data-change. `OperationCompleted` keeps the public event
shape, but no trigger has been modeled yet because the capture set contains no
`mx.event.operation-complete` events.
`DataChanged` is also suppressed when the lower-level decoder reports a known
wire kind with a `null` value, which is how malformed/incomplete callback
payloads such as the observed string-array records from captures `100` and
`101` are represented.
`GalaxyRepositoryUserResolver` is separate metadata support: it resolves
`dbo.user_profile` rows, profile IDs, default security group, InTouch access
level, and decoded role names from the GR role blob. `AddItem2` currently
combines context and item text only for simple relative references; the real
MXAccess context behavior still needs captures. Unsupported MXAccess methods
throw `NotSupportedException` with the missing capture/protocol reason.
For lower-level use, `GalaxyRepositoryUserResolver` exposes:
```csharp
var users = new GalaxyRepositoryUserResolver();
GalaxyUserProfile profile = await users.ResolveByNameAsync("Administrator");
int profileId = await users.ResolveUserProfileIdByGuidAsync(profile.UserGuid);
IReadOnlyList<string> roles = profile.Roles;
```
Security-enabled `AuthenticateUser` password verification remains intentionally
out of scope until successful and failed captures from a security-enabled Galaxy
show whether MXAccess delegates to OS/domain auth, GR hashes, or another token
provider.
## Current limitations
- Managed NTLM live probes now validate .NET 10 x64 activation, RemQI,
`GetPartnerVersion`, direct `WriteAsync` transport, callback delivery, and
unsubscribe cleanup with unique local engine IDs.
- Value-bearing managed `OnDataChange` is proven for
`TestChildObject.ShortDesc` after matching native advise transfer kind and
value-handle property id. The lower-level session exposes that callback, but
the MXAccess-style compatibility wrapper suppresses the empty
`InternationalizedString` public `DataChanged` event because fresh x86
MXAccess capture `108` also raises no public event for `ShortDesc`. Fresh
current-state x86 MXAccess captures also raise no public `TestInt`
data-change for subscribe-only or subscribe-then-write, matching the managed
status-only `TestInt` callbacks on this VM. Broader scalar/array parity still
needs live validation against a runtime state that emits public values.
Replaying the captured
adapter-visible `0x17` metadata body is accepted by `NmxSvc` and produces
decoded `0x40` metadata-response plus `0x32` metadata-status callbacks, but
the response still contains an internal base-object error and does not appear
to be the gate for the `ShortDesc` value callback.
- `ReadAsync` currently uses a transient subscription. A lower-latency direct
read path can be added if a distinct NMX request/response read body is found;
tags with no initial value can still time out after receiving only status
callbacks.
- String-array callbacks are partially modeled from the element format, but the
available Frida string-array callback line is truncated before all values.
- Error/failure callback bodies still need more captures for exact
`MXSTATUS_PROXY[]` parity.
- `ElapsedTime` and `InternationalizedString` writes are value-projected from
captures `095` and `096`. The managed `ShortDesc` write body now matches
capture `096` byte-for-byte and returns the same completion-only `0xef` as
native MXAccess on this node, while `TestInt` returns completion-only `0x00`
and a status-only update callback. Public write-complete semantics for these
completion-only frames still need more captures.
- `MxNativeCompatibilityServer` provides source-level migration help but is not
a binary COM replacement for `ArchestrA.MxAccess`.
- For invalid references, the compatibility wrapper intentionally differs from
strict `MxNativeSession`: it returns an item handle and raises the observed
public configuration-error data-change (`null`, quality `0`, detail `6`) on
advise, matching x86 MXAccess captures.
+670
View File
@@ -0,0 +1,670 @@
# 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:
```text
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:
```text
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:
```text
DllCanUnloadNow
DllGetClassObject
DllRegisterServer
DllUnregisterServer
```
It imports the expected RPC proxy/stub helpers from `RPCRT4.dll`:
```text
NdrDllGetClassObject
NdrDllRegisterProxy
NdrDllUnregisterProxy
CStdStubBuffer_*
IUnknown_*_Proxy
```
## Key NMX service contracts
The already decompiled interop tree contains the type-library contracts:
```text
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:
```csharp
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 by `body_length` bytes.
- A positive control `code_or_status` often announces the total byte count of
one or more following data records.
- `-1` appears as a normal acknowledgement/status control.
- `-2` appears as a bidirectional status/control marker around write windows.
The parser for this stream is:
```text
analysis\scripts\decode_mixed_local_stream.py
```
Generated mixed-stream decodes:
```text
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:
```text
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:
```text
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:
```text
analysis\frida\write-body-matrix.tsv
```
Array writes are also captured:
```text
analysis\frida\write-array-body-matrix.tsv
```
Write-mode captures are saved at:
```text
analysis\frida\write-mode-matrix.tsv
```
The first combined Frida plus loopback correlation is:
```text
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:
```text
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:
```text
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`:
```text
captures\046-service-boundary-write-test-int-123456791
```
The same 86-byte write body appears at:
```text
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:
```text
src\MxNativeClient.Probe
```
The same probe can marshal the activated object as remote `IUnknown` and dump a
standard OBJREF:
```text
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:
```text
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:
```text
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`:
```text
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:
```text
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:
```text
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:
1. Recreate enough of the NMX service/session behavior represented by
`INmxService`, `INmxSvcCallback`, and `INmx4`.
2. Encode/decode the NMX adapter message bodies identified by the Frida trace.
3. Replace the 32-bit `NmxSvcps.dll` MIDL proxy with a managed NDR/DCOM proxy
for `NmxSvc.exe`.
4. 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:
```text
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:
```text
analysis\scripts\extract_nmxsvcps_proc_formats.py
```
The output is:
```text
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:
```text
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:
```text
analysis\frida\nmx-com-proxy-trace.js
```
Captured runs:
```text
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:
```text
char_count:uint32
byte_length:uint32
char_count:uint32
utf16_payload_without_null
```
For `NmxComProxyWire5`, the exact captured bytes were:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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`:
```text
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:
```text
managed_register2_callback_hresult=0x00000000
managed_unregister_after_callback_register_hresult=0x00000000
```
A self-directed `TransferData` probe also returns `0x00000000`:
```text
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:
```text
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:
```text
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:
```text
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.
+80
View File
@@ -0,0 +1,80 @@
# Transport correlation
This note records the current boundary between the native adapter body format
and localhost transport.
## Combined captures
The combined runner starts Npcap loopback capture, then launches the harness
under Frida:
```text
analysis\scripts\run_frida_loopback_capture.ps1
```
Helper scripts:
```text
analysis\scripts\map_frida_to_tcp.py
analysis\scripts\parse_dcerpc_streams.py
analysis\scripts\decode_mixed_local_stream.py
```
## Capture 043
```text
captures\043-frida-loopback-write-test-int-115
```
This writes `TestChildObject.TestInt = 115`. It proved exact Frida adapter
bodies are not copied verbatim to TCP, but the scalar value `115` was ambiguous
because it also matched DCE/RPC call IDs in the same window.
## Capture 044
```text
captures\044-frida-loopback-write-test-int-123456789
```
This writes a distinctive value:
```text
TestChildObject.TestInt = 123456789
```
Results:
| Needle | Result |
| --- | --- |
| raw little-endian `123456789` | not found anywhere in the full pcap payload scan |
| exact 40-byte Frida `PutRequest` body | not found in reassembled TCP streams |
| exact 86-byte Frida `TransferData` body | not found in reassembled TCP streams |
| exact 88-byte Frida callback body | not found in reassembled TCP streams |
| mixed `127.0.0.1:57415 <-> 57433` stream | parsed, raw value not found |
| DCE/RPC `::1:49704` streams | parsed 452 PDUs, raw value not found in request/response stubs |
Generated files:
```text
captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv
captures\044-frida-loopback-write-test-int-123456789\dcerpc-stream-pdus.tsv
captures\044-frida-loopback-write-test-int-123456789\mixed-stream-57415-to-57433.tsv
captures\044-frida-loopback-write-test-int-123456789\mixed-stream-57433-to-57415.tsv
```
## Implication
The `CNmxAdapter::PutRequest` and `CNmxAdapter::TransferData` buffers are an
internal adapter representation, not the TCP wire format. The wire transport
does not expose the write value as plain little-endian scalar bytes for this
distinctive-value capture.
The next reverse-engineering step is to decode the structural bridge between
adapter bodies and transport messages:
1. Correlate Frida call timestamps to DCE/RPC call IDs and mixed-stream record
windows.
2. Decode DCE/RPC NDR stubs for the observed context/opnum pairs.
3. Hook deeper in `NmxSvc.exe` around `CNmxControler::TransferData` and
`CNmxService::TransferData` so both sides of the adapter-to-service boundary
can be compared before TCP serialization.