Files
mxaccess/docs/MXAccess-Public-API.md
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
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>
2026-05-05 06:21:00 -04:00

24 KiB

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:

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:

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:

public short success;
public MxStatusCategory category;
public MxStatusSource detectedBy;
public short detail;

MxStatusCategory:

Unknown = -1
Ok = 0
Pending = 1
Warning = 2
CommunicationError = 3
ConfigurationError = 4
OperationalError = 5
SecurityError = 6
SoftwareError = 7
OtherError = 8

MxStatusSource:

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