Files
mxaccess/design/followups.md
T
Joseph Doherty ecfcc3f429 [M3] mxaccess-rpc: NmxService2 codec + F9 ResolveOxid wrappers
Two units of work in one commit:

1. nmx_service2_messages.rs (~470 LoC, 18 tests) — port of
   NmxService2Messages.cs. Encoders for all 9 INmxService2 opnums
   (RegisterEngine, UnRegisterEngine, Connect, TransferData,
   AddSubscriberEngine, RemoveSubscriberEngine, SetHeartbeatSendInterval,
   RegisterEngine2, GetPartnerVersion) plus BSTR + InterfacePointer NDR
   helpers used by RegisterEngine2 marshalling. Decoders for the
   GetPartnerVersion result and the generic HRESULT response. M3 stream
   B (NmxClient) will be a thin layer over these + the transport.

2. object_exporter_client.rs (~290 LoC, 6 tests including 2 real-socket
   tokio tests) — resolves followup F9. Implements:
   - resolve_oxid_unauthenticated (cs:14-30)
   - resolve_oxid_with_managed_ntlm_packet_integrity (cs:66-81)
   ResolveOxidOutcome enum disambiguates the two response shapes the
   .NET reference parses (typed result vs 4-byte failure). The two SSPI
   flavours (cs:32-47, cs:49-64) are permanently skipped — they wrap
   .NET-only System.Net.Security.SspiClientContext.

design/followups.md: F9 moved to Resolved with this commit's hash.

Test count delta: 364 -> 389 (+25; mxaccess-rpc 137 -> 162; +18 from
nmx_service2_messages, +7 from object_exporter_client which includes
the +2 fall-through tests for the dual-shape response decoder).
Open followups touched: F9 resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:56:11 -04:00

7.1 KiB

Followups

Open work items deferred during /loop iterations. Triaged at the top of every iteration. New items are appended under ## Open; resolved items move to ## Resolved with a date + commit hash.

Open

F1 — NTLM consumer-layer helpers (workstation default + from_env constructor)

Severity: P3 Source: M2 wave 1, crates/mxaccess-rpc/src/ntlm.rs Why deferred: The .NET reference's Environment.MachineName default for workstation and its FromEnvironment() constructor (ManagedNtlmClientContext.cs:38, :41-49) read host state and env vars — both side effects that don't belong in a pure codec module. The constructor takes workstation: Option<&str> so callers can wire either later. Resolves when: M2 wave 2 transport (or the M2 example connect-nmx.rs) wires NtlmClientContext::new(.., Some(hostname()?)) and provides a small from_env helper at the consumer layer.

F2 — NTLM verify_signature path + constant-time MAC compare (server-to-client direction)

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/ntlm.rs Why deferred: The .NET ManagedNtlmClientContext only implements client-to-server signing (cs:30,124); there is no implementation of server-to-client sign/seal keys or verify_signature. Both are needed when the callback exporter receives a signed inbound frame from NmxSvc.exe, but no such fixture exists yet. Resolves when: M2 wave 3 (callback exporter) captures an INmxSvcCallback::StatusReceived frame with an auth_value trailer per design/60-roadmap.md:56 (DoD #3) and a fixture lands under tests/fixtures/m2-status-frame/. Add subtle = "2" and gate the byte compare behind ConstantTimeEq at the same time.

F3 — Cross-domain NTLM Type1/2/3 fixture

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/ntlm.rs Why deferred: All current NTLM fixtures are single-domain (the local AVEVA install). Tracked separately in design/70-risks-and-open-questions.md R8 (P1 risk) and the open-evidence-gaps table. Resolves when: A multi-domain AVEVA test harness lands and a successful cross-domain authenticate round-trip captures Type1/2/3 bytes. Notes: this clears R8.

F4 — BindAck / AlterContextResponse body parser

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/pdu.rs Why deferred: The .NET reference (DceRpcPdu.cs:217-262) parses Bind and AlterContext into the same struct but does not decode the corresponding response body (result list + secondary address). The Rust port's BindPdu::decode accepts BindAck packet type but does not interpret the body. The negotiated transfer syntax — needed before opnum dispatch — is currently inferred from request-side context. Resolves when: A captured BindAck frame from captures/013-loopback-subscribe-scalars/nmx-stream-*.bin is decoded and the body shape is documented in docs/Loopback-Protocol-Findings.md.

F5 — Captured DCE/RPC bind-frame fixture round-trip

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/pdu.rs Why deferred: Existing PDU tests build hand-constructed [C706]-conformant frames. A capture-driven round-trip (extract bind/alter PDUs from captures/013-loopback-subscribe-scalars/nmx-stream-*.bin, decode → encode → assert byte-identical) would be stronger evidence of parity with the live wire. Resolves when: Bytes from that capture are extracted into tests/fixtures/m2-pdu/ and the round-trip test lands.

F6 — Port ComObjRefProvider.cs (OBJREF emitter via Win32 CoMarshalInterface)

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/objref.rs Why deferred: The provider is a wrapper around ole32::CoMarshalInterface / IStream / GlobalLock / GlobalSize. It needs windows-rs, which is currently behind the windows-com feature in mxaccess-rpc/Cargo.toml. The pure-Rust parser stands alone for the inbound activation-response path that M2 wave 1 needs. Resolves when: windows-rs is wired into mxaccess-rpc (M2 wave 3 callback exporter needs to publish its own OBJREF for IRemUnknown / INmxSvcCallback registration) and an emitter port lands behind the windows-com feature.

F10 — IObjectExporter::ResolveOxid2 (opnum 4) body codec

Severity: P2 Source: M2 wave 2, crates/mxaccess-rpc/src/object_exporter.rs Why deferred: ObjectExporterMessages.cs only models opnum 0 (ResolveOxid). Opnum 4 (ResolveOxid2) has a different response shape — it adds a COMVERSION plus an AuthnHnt[] array. The .NET reference does not exercise this path, so there's no executable spec to mirror. Resolves when: Either a [MS-DCOM] §3.1.2.5.1.4-derived layout is verified against a captured ResolveOxid2 exchange, or the .NET reference grows a ParseResolveOxid2* helper.

F11 — IRemUnknown::RemAddRef and RemRelease body codecs

Severity: P2 Source: M2 wave 2, crates/mxaccess-rpc/src/rem_unknown.rs Why deferred: RemUnknownMessages.cs declares the opnums (:9-10) but does not implement encoders/decoders. The Rust port matches that exactly per "port what is already proven." Resolves when: The .NET reference adds bodies for opnums 4 / 5 (or a captured frame establishes the on-wire shape). At that point port them into rem_unknown.rs alongside the existing RemQueryInterface codec.

Resolved

F7 — Consolidate Guid type across mxaccess-rpc

Resolved: 2026-05-05 in this iteration's commit. Guid was hoisted from objref::Guid into the new shared crate::guid::Guid module. objref and pdu now re-export from there; M2 wave 2's orpc, object_exporter, and rem_unknown import it directly. The OXID-resolve dual-string decoder additionally needs an owned protocol label (format!("protseq_0x{:04x}", tower_id) per ObjectExporterMessages.cs:120) — ComDualStringEntry::protocol was upgraded from &'static str to Cow<'static, str> to support both decoders without the agent's interim Box::leak workaround.

F8 — RpcError is duplicated across objref and pdu modules

Resolved: 2026-05-05 in this iteration's commit. RpcError was hoisted into the new shared crate::error::RpcError module as a single union of all wave 1 variants plus a generic Decode { offset, reason: &'static str, buffer_len } variant for the wave 2 ORPC parsers' one-off failures. objref and pdu re-export from there; M2 wave 2's orpc, object_exporter, and rem_unknown use it directly.

F9 — ObjectExporterClient.cs ResolveOxid wrapper methods

Resolved: 2026-05-05. Both portable methods land in crates/mxaccess-rpc/src/object_exporter_client.rs: resolve_oxid_unauthenticated (mirrors cs:14-30) and resolve_oxid_with_managed_ntlm_packet_integrity (mirrors cs:66-81). Each opens a TCP connection, binds to IObjectExporter, calls opnum 0 with the encoded request, and decodes the response — preferring parse_resolve_oxid_result then falling back to parse_resolve_oxid_failure for short stubs. The two SSPI flavours (ResolveOxidWithNtlmConnect, ResolveOxidWithNtlmPacketIntegrity) wrap .NET's System.Net.Security.SspiClientContext and are explicitly out of scope for the Rust port — that's a permanent skip, not a deferral.