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>
57 KiB
Adversarial design review
Generated: 2026-05-05. Reviewer: Claude general-purpose subagent (per-file, hostile framing).
This is a challenge review — not a style pass. Reviewers are instructed to question implementation choices, design tradeoffs, and assumptions; verify every load-bearing claim against the cited evidence in src/ (.NET reference), docs/, analysis/, captures/; and surface where the design could fail under real-world conditions.
Status (2026-05-05)
All findings have been addressed across three triage passes.
| Severity | Count | Status |
|---|---|---|
[BLOCKER] |
24 | All resolved (cluster pass 1) |
[MAJOR] |
~26 | All resolved (cluster pass 2) |
[MEDIUM] |
3 | All resolved (cluster pass 1) |
[MINOR] |
~14 | All resolved (cluster pass 3) |
[NIT] |
~7 | All resolved (cluster pass 3) |
Each finding bullet below is prefixed with [RESOLVED] to indicate the design doc has been corrected. The original finding text is preserved verbatim as the audit trail; see git log design/*.md for the specific edits. New risks added in response: R13 (recordCount != 1 panic risk), R14 (fabricated 0x80004021 → StaleItem mapping), R15 (Drop-time async cleanup hazards), R16 (crypto/auth crate maintenance drift). Severity tiers (P0/P1/P2) were added to all R-items.
Severity-tag legend (in original review):
- BLOCKER — must fix before implementation; protocol or safety bug, or load-bearing claim that is fabricated / contradicts evidence.
- MAJOR — load-bearing assumption that is unsupported, under-specified, or likely wrong.
- MEDIUM — moderate-severity finding falling between MAJOR and MINOR.
- MINOR — clarity, consistency, or naming.
- NIT — style / preference.
design/00-overview.md
- [RESOLVED]
[BLOCKER]Doc listsregister, write, advise, readasTransporttrait primitives (design/00-overview.md:61) and the public API (design/10-raw-layer.md "Read" section, line 199) confirmsReadAsyncis "implemented as a transient subscription read". Puttingreadon theTransporttrait alongside the actual wire primitives misrepresents the protocol — there is no NMX read primitive.MxNativeSession.ReadAsync(src/MxNativeClient/MxNativeSession.cs:312–351) implements read asSubscribeAsync+ first-callback-result + dispose. If theTransporttrait shape impliesreadis transport-level, every transport must reimplement the subscribe-then-cancel dance instead of getting it once at the session layer. This contradicts principle 1 ("do not fabricate protocol behavior") by inventing a wire primitive that does not exist. - [RESOLVED]
[MAJOR]Diagram labelsmxaccess-asb-soapas "NetTcp + SOAP" with "DH/HMAC/AES" (design/00-overview.md:75–77). The DH/HMAC/AES claim is supported (src/MxAsbClient/AsbSystemAuthenticator.cs:23–34, 73–122). But the "SOAP" label is misleading: the .NET reference usesNetTcpBinding(SecurityMode.None)with the default binary message encoder (src/MxAsbClient/MxAsbDataClient.cs:660–663). SOAP envelope bytes only exist as an in-memory infoset; on the wire it's MS-NMF-framed binary XML. design/70-risks-and-open-questions.md:9–18 (R1) confirms the plan is to "hand-roll the framing per [MS-NMF]" — no SOAP text framing. Crate namemxaccess-asb-soapand the diagram label will mislead implementers into thinking SOAP envelopes are on the wire. - [RESOLVED]
[MAJOR]Principle 3 says "Raw layer isunsafe-free. No raw pointers, notransmute, no FFI in the public surface" (design/00-overview.md:33), but principle 6 mandateswindows-rsfor "OBJREF building, IPID/OXID/OID handling, GUID literals" (line 36) and the diagram showsmxaccess-rpcconsumingwindowscrate (line 88). Everywindows-rsCOM call isunsafe fn. The .NET reference usesType.GetTypeFromProgID+Activator.CreateInstance(src/MxNativeClient/ManagedNmxService2Client.cs:33–36);windows-rsexposes equivalents only viaunsafe { CoCreateInstance(...) }. The doc never reconciles this — either "raw layer isunsafe-free in the public surface" (i.e. internalunsafeallowed) orwindows-rstypes stay out ofmxaccess-rpc. As written, principles 3 and 6 are in direct tension. - [RESOLVED]
[MAJOR]Principle 8 says "No spawn from insideDrop; no blocking calls insideasync fn" (design/00-overview.md:38) but principle 2 in "Async layer" (line 10) promises "drop … cancellation" and 70-risks-and-open-questions.md:24 / R3 design referencesStreamcleanup. Drop-based cancellation of an in-flight subscription onNmxSvcrequires sending anunadvise/UnregisterReferenceframe — that is I/O. IfDropcannot spawn and cannot block, the only options are (a) abandon the server-side subscription leak (NMX leak in the service process), (b) require an explicitclose().awaitand downgradeDropto a panic-in-debug, or (c) hand the cleanup to a background reaper task (which itself was spawned at session start, not inside Drop). The doc never specifies which, and "drop cancellation" implies (a)/(c) without saying so. CompareMxNativeSession.DisposeAsync/explicit unregister flows (src/MxNativeClient/MxNativeSession.cs usesVolatile, recovery, etc.) — none areDrop-equivalent. - [RESOLVED]
[MAJOR]Crate-mapping table (design/00-overview.md:22–25) splitsMxNativeClientintomxaccess-rpc,mxaccess-nmx,mxaccess-callbackfor transport and putsMxNativeSessionandMxNativeCompatibilityServersolely in the asyncmxaccesscrate ("raw layer ends at transport"). But the .NETMxNativeSessionis the only place where Galaxy resolution + handle lookup + correlation-id bookkeeping + recovery state are wired (src/MxNativeClient/MxNativeSession.cs:90–125, 312–351, 573). Ifmxaccess-nmxexposes only "INmxService2 client + envelope" (design/00-overview.md:69), thenSessionmust reimplement every cross-cutting concern (subscription registry, recovery, callback routing) inside the async crate. Either the raw layer is incomplete (cannot be used standalone for "byte-level control" as line 12 promises, because there's no session/correlation surface), or the split is wrong. The 1:1 mapping in the table papers over this. - [RESOLVED]
[MEDIUM]Principle 9 says "ASB-only paths returnError::UnsupportedonNmxTransportand vice versa; capability is queryable" (design/00-overview.md:39), but non-goal #5 (line 48) says "the Rust port routes those [callback-only ops] to NMX; ASB owns the regular tag data plane only". A consumer holding anAsbTransportand callingactivate(item)will getError::Unsupported— but the natural expectation (set by the non-goals paragraph) is that the high-levelmxaccessSessiontransparently routes callback-only ops to NMX even when ASB is the data plane. The doc never says how the dual-routing works at theSessionlevel — does the session hold both transports? Is the user expected to construct two? Principle 9 ("two transports, one façade") and non-goal 5 are in tension. - [RESOLVED]
[MEDIUM]Line 16: "every async method bottoms out in a sync codec call (NmxTransferEnvelope.Encode)". Searched src/MxNativeCodec — there is noNmxTransferEnvelope.Encode. The class isNmxTransferEnvelopeTemplatewithEncode(ReadOnlySpan<byte>)(src/MxNativeCodec/NmxTransferEnvelopeTemplate.cs:33). Minor naming drift, but in a doc whose principle 1 is "do not fabricate" and which cites filenames as evidence, citing a non-existent method weakens the rhetorical case. - [RESOLVED]
[MEDIUM]Principle 7 citesdbo.gobject/dbo.instance/dbo.dynamic_attributeas the SQL surface (design/00-overview.md:37). Verified at src/MxNativeClient/GalaxyRepositoryTagResolver.cs:215, 253, 257. However CLAUDE.md (project root) lists the surface asaa_attribute/aa_object/mx_attribute_category. Grepping the.csfile shows zero hits for any of those names. CLAUDE.md is wrong, the design doc is right, but the design doc should call this out as a CLAUDE.md correction — otherwise the next implementer who trusts CLAUDE.md will write SQL against tables that don't exist.
design/10-raw-layer.md
- [RESOLVED]
[BLOCKER]Doc Item-control table claimsAdvise (plain)opcode0x1f= 37 bytes, distinct fromAdviseSupervisory0x1f= 39 bytes (design/10-raw-layer.md:99-104). The .NET reference does not support a 37-byte plain-advise variant:NmxItemControlMessage.Parseaccepts onlyAdviseSupervisoryorUnAdvise(src/MxNativeCodec/NmxItemControlMessage.cs:46-48), andMxNativeCompatibilityServer.AdviseSupervisoryaliases plainAdviseto it (src/MxNativeClient/MxNativeCompatibilityServer.cs:256-258). The doc's three-row table fabricates a "plain advise" length the codec rejects. - [RESOLVED]
[BLOCKER]Doc claims Boolean write body has "1 value byte (0xFF/0x00)" giving 37 bytes total (design/10-raw-layer.md:114). Source actually emits 4 value bytes:[0xff,0xff,0xff,0x00]or[0x00,0xff,0xff,0x00](src/MxNativeCodec/NmxWriteMessage.cs:257). Total still 37 because the suffix is 11+4 (not 14+4), but the per-byte breakdown in the doc is wrong — the "1 value byte" claim will lead a Rust port to emit a 34-byte body that the receiver rejects. - [RESOLVED]
[MAJOR]Doc String-write row says "Total = 26 + N" (design/10-raw-layer.md:118). True size isKindOffset(17)+1+4+4+N+14+4 = 44+N(src/MxNativeCodec/NmxWriteMessage.cs:150). The row drops the 14+4 suffix that every other row in the same table includes — an inconsistency in the column meaning that will produce undersized buffers. - [RESOLVED]
[MAJOR]Doc array-body row says "28 + N + 18" with layout "4-byte marker + count(u16) + width(u16) + elements" (design/10-raw-layer.md:120). Code shows count is at body offset 22 and width at 24, with two separate gap regions: bytes 18-21 are zero-padding and bytes 26-27 are zero-padding (src/MxNativeCodec/NmxWriteMessage.cs:179-184). The "4-byte marker" framing implies a single contiguous marker before count; in reality it's a 4-byte gap and a 2-byte gap surrounding the count/width. Documenting it that way will round-trip wrong on captures. - [RESOLVED]
[MAJOR]Doc claimsMxStatus.source: MxStatusSource(design/10-raw-layer.md:178). The .NET reference field name isDetectedBy(src/MxNativeCodec/MxStatus.cs:31). Drift in field name across the parity boundary; tests that round-trip serialize will diverge. - [RESOLVED]
[MAJOR]Doc claims wire kinds0x41–0x46for arrays inMxValue/write (design/10-raw-layer.md:120, 149). Encoder side never emits0x46—NmxWriteMessage.GetWireKindcollapsesStringArrayandDateTimeArrayboth to0x45(src/MxNativeCodec/NmxWriteMessage.cs:107). Decoder side does treat0x46as DateTimeArray (src/MxNativeCodec/NmxSubscriptionMessage.cs:173, 275). The doc lumps both directions as "0x41..0x46", masking an asymmetry the Rust port must replicate. - [RESOLVED]
[MAJOR]Doc says "DurationforElapsedTime: 4-byte milliseconds on the wire" (design/10-raw-layer.md:220). Source decodes the wire as signedi32(BinaryPrimitives.ReadInt32LittleEndian, src/MxNativeCodec/NmxSubscriptionMessage.cs:252) and producesTimeSpan.FromMilliseconds(milliseconds). Ruststd::time::Durationis unsigned — a negative ms value (which the encoding allows) will panic or be clamped. Either spec a signed type (time::Durationori64ms) or document the negative-handling policy. - [RESOLVED]
[MAJOR]Doc DataUpdate parser sketch saysrecordCount(i32, typically 1)and shows genericrecords[recordCount](design/10-raw-layer.md:144-147). The .NET reference hard-rejects any record count != 1:if (recordCount != 1) throw(src/MxNativeCodec/NmxSubscriptionMessage.cs:71-74). The Rust sketch implies general support for N records that the executable spec does not provide; either lift the constraint with capture evidence or document it as an asserted invariant. - [RESOLVED]
[MAJOR]Doc says NTLM Type1 negotiate flags are "Unicode | RequestTarget | Sign | Seal | ExtendedSessionSecurity | Negotiate128 | KeyExchange" (design/10-raw-layer.md:246). SearchedManagedNtlmClientContext.csfor the actual flag set used in the negotiate emission — the file does derive sign/seal/sequence keys (src/MxNativeClient/ManagedNtlmClientContext.cs:177-200) and uses_user.ToUpperInvariant()for response key derivation (line 79), but the doc lists no citation for the Type1 flag bitfield. No line/offset is referenced; the flag list is a claim a Rust port could mis-encode without a fixture. Add a citation or capture. - [RESOLVED]
[MAJOR]Doc putsNmxTransferEnvelope.reservedasi32"preserved from observed; default 0" (design/10-raw-layer.md:78-79). The .NET encoder unconditionally writes 0 there (src/MxNativeCodec/NmxTransferEnvelope.cs:91) andParsedoes not extract or expose the reserved bytes at all — there is no preservation. The doc's "round-trip preserver" promise (design/10-raw-layer.md:92) cannot be satisfied unless the Rust codec adds a field the .NET reference has thrown away. Either fix the .NET reference or drop the claim. - [RESOLVED]
[MAJOR]Doc says SubscriptionStatus hasrecordCount(i32) + operationId(GUID 16) + correlationId(GUID 16) + records[recordCount]immediately aftercmd+version(design/10-raw-layer.md:135-140). Source orders fields the same but readsrecordCountat offset 3,operationIdat 7,correlationIdat 23, records at 39 (src/MxNativeCodec/NmxSubscriptionMessage.cs:54-55, 98-99). Total agrees, but the diagram's+ correlationIdincludes records-bearing offsets that the .NET parser splits into two distinct paths (DataUpdate has no correlationId; SubscriptionStatus does). Doc places correlationId in both records (line 135-140 union) — Rust port must not parse correlationId for0x33. - [RESOLVED]
[MAJOR]Doc warns about.NET ToLowerInvariant()vs Ruststr::to_lowercase()Unicode divergence and proposes a_legacyvariant using "unicase::Ascii::to_lowercase" (design/10-raw-layer.md:66-68).unicase::Ascii::to_lowercaseonly handles ASCII — it is not a substitute forToLowerInvariant()on non-ASCII Galaxy tag names. If the captured tags include non-ASCII (Turkish, German), the proposed legacy fallback will produce a different CRC than the .NET reference, not the same one. The mitigation as written makes the divergence worse. - [RESOLVED]
[MAJOR]MxReferenceHandleRust struct usespubfor every primitive field including fields recomputed from name signatures (design/10-raw-layer.md:28-41). Theoriginal: Bytespreservation pattern (design/10-raw-layer.md:226) cannot apply here because the struct isCopywith no buffer. A consumer mutatingobject_signaturedirectly will desync fromcompute_name_signature(tag_name)— there is no invariant binding the two together. Either expose the handle as opaque with accessors, or document that signatures are caller-owned and the codec will not recompute. - [RESOLVED]
[NIT]Doc names callback opnumsDataReceivedRaw(3) andStatusReceivedRaw(4) (design/10-raw-layer.md:296). Source names themDataReceived/StatusReceived(src/MxNativeClient/NmxSvcCallbackMessages.cs:11-12, NmxProcedureMetadata.cs:89-101). TheRawsuffix is doc-invented, will diverge from any cross-reference grep against the .NET reference. - [RESOLVED]
[NIT]Doc claims theElapsedTimewire kind0x07is part of the array set "0x41..0x46" decoded scalars (design/10-raw-layer.md:149). It is in the scalar set 0x01..0x07 — butMxValueKinddoes not enumerateElapsedTimeArray, whileMxValuealso lacks one (design/10-raw-layer.md:200-215). If0x47ever appears in a capture, both .NET and the proposed Rust enums would silently drop it. Worth flagging as a known gap.
design/20-async-layer.md
- [RESOLVED]
[BLOCKER]Session::writeclaims a single&strreference is sufficient, butMxNativeSession.WriteAsyncrequireswriteIndexandclientTokenparameters that drive correlation ofOperationStatuscallbacks (src/MxNativeClient/MxNativeSession.cs:165-185). The Rust API at design/20-async-layer.md:32-49 has no clientToken, so theawaitcannot await a wireWriteCompleted; it can only confirm the LMXWriteRPC return code. This contradicts the claim that "write" is a true async operation that completes when the wire confirms — section 310 even concedes the 5-byte completion frame is observed-only. The single-shotawaitmodel is misleading. - [RESOLVED]
[BLOCKER]write_securedis offered unconditionally (design/20-async-layer.md:40-45) but the .NET reference explicitly throwsNotSupportedExceptionfor it, citing0x80004021from captures 036/038/039 (MxNativeSession.cs:211-221). The Rust API surface promises a behaviour the proven stack does not deliver. Either drop it or rename towrite_secured2to matchWriteSecured2Async. - [RESOLVED]
[BLOCKER]readis described as a one-shotread(&str) -> DataChange(design/20-async-layer.md:47-49) with no timeout argument. The .NET reference'sReadAsyncis explicitly a timed advise/first-callback/unadvise dance and requiresTimeSpan timeout > 0(MxNativeSession.cs:312-359). The Rust API has no semantic for the case where no callback ever arrives —tokio::time::timeoutcan drop the future, but on drop the Rust design does not say it issuesUnAdvise(onlySubscriptiondrop does). This leaks an advise on every read timeout. - [RESOLVED]
[MAJOR]Subscriptionis declaredStream<Item = Result<DataChange, Error>>(design/20-async-layer.md:70) without specifying whetherErris terminal. The .NET reference fans out records viaCallbackReceivedand routes parse errors to a separateUnparsedCallbackReceivedevent (MxNativeSession.cs:590-607). The Rust design conflates these. IfErrterminates the stream, transient parse errors kill an otherwise healthy subscription; ifErris recoverable, downstreamwhile let Some(Ok(_))consumers silently skip data. Pick one and document, or split into two streams matching .NET. - [RESOLVED]
[MAJOR]Drop-cancellation ofSubscriptionclaims to "sendUnAdvise(best-effort, fire-and-forget viatokio::spawn)" (design/20-async-layer.md:70). On runtime shutdowntokio::spawnfrom aDropimpl panics if no runtime is current, and duringRuntime::shutdown_timeoutspawned tasks are aborted before they can flush. The .NET reference disposes synchronously, sendingUnAdviseper subscription on the same thread (MxNativeSession.cs:483-495). Document the runtime-shutdown path or provide an explicitasync fn close(). - [RESOLVED]
[MAJOR]Session: Clone + Send + Syncwith sharedArc<SessionInner>(design/20-async-layer.md:27) but no explicitclose()API. Last-clone-drop runningunregister_engine"best-effort" requires eithertokio::spawn(same shutdown hazard as above) orblock_on(forbidden by section 305). The .NET reference isIDisposablesynchronous and unregisters explicitly. The design has no answer for "I want to make sure the engine is unregistered before my process exits." - [RESOLVED]
[MAJOR]Recovery is presented as automatic on heartbeat-loss (design/20-async-layer.md:114), butMxNativeSession.RecoverConnection*is explicitly caller-driven — the .NET API exposesRecoverConnectionAsync(policy)and never auto-starts (MxNativeSession.cs:383-440). Worse: during recovery,_recoveryActiveis just a flag set on inbound callbacks; in-flight writes against_serviceare not paused or replayed. The Rust design's promise that "the future resumes on the new connection" is unbacked — port the .NET semantics (concurrent calls fail, caller decides) or capture the gap. - [RESOLVED]
[MAJOR]subscribe_buffered(design/20-async-layer.md:87-100) returnsStream<DataChangeBatch>but the .NET equivalentRegisterBufferedItemAsynctakesitemDefinition,itemContext, and crucially anitemHandle: int(MxNativeSession.cs:272-310). The Rust API drops the handle and the(definition, context)split, hiding the dual-string requirement. A consumer cannot reproduce the captured Frida bodies through this API. - [RESOLVED]
[MAJOR]subscribe_many(&["A.X","A.Y","A.Z"])is described as multiplexing one callback channel and demultiplexing by correlation ID (design/20-async-layer.md:74-82). The .NET reference issues per-tagAdviseSupervisorywith oneCorrelationIdeach (MxNativeSession.cs:250-270); there is no atomicity. If the secondAdviseerrors, the design does not specify whether the first is rolled back. A consumer expects either all-or-nothing or partial success surfaced — the doc says neither. - [RESOLVED]
[MAJOR]Transporttrait uses#[async_trait](design/20-async-layer.md:168), forcing heap allocation per call and breaking the recently-stabilized nativeasync fnin trait. If the project pivots to dyn-compatible native AFIT (Rust 1.75+ requiresdyn Transportto usePin<Box<dyn Future>>returning fns), the trait is not dyn-safe as written becausecallbacks(&self) -> CallbackStreamreturns a concrete struct — fine — butasync fnmethods are not dyn-safe without RPITIT workarounds. Pick#[async_trait](legacy) or document thatTransportis generic-only. - [RESOLVED]
[MAJOR]RecoveryEventenum (design/20-async-layer.md:122-127) is missingWillRetry: boolfromMxNativeRecoveryFailureEvent(MxNativeSession.cs:47-51). Consumers cannot distinguish "this attempt failed but the policy will retry" from "terminal failure." Without it, downstream code cannot decide when to tear down its own state. - [RESOLVED]
[MAJOR]quality: u16onDataChange(design/20-async-layer.md:222) but the .NETMxStatusmodel has its own categories (MxStatusCategory/MxStatusSource) and the codec already exposesMxStatus. Exposing a rawu16next tostatus: MxStatusinvites callers to use the wrong field — the .NET reference usesRecord.ToDataChangeStatus()(MxNativeSession.cs:70) as the canonical projection. Drop theu16or document the precedence. - [RESOLVED]
[MINOR]set_recovery_policytakes&mut sessionin the sample (design/20-async-layer.md:288) butSessionisCloneandArc-backed (:27). Mutation through&muton a clone is a foot-gun: clones won't see policy changes unless they're stored behind interior mutability. Either make it&selfwithtokio::sync::watchor document that it must be called before any clone is made. - [RESOLVED]
[MINOR]&'static strinError::Unsupported(design/20-async-layer.md:204) prevents formatting the offending operation with runtime context (e.g. capability name + transport variant). UseCow<'static, str>or a structured variant.
design/30-crate-topology.md
- [RESOLVED]
[BLOCKER]quick-xmlis the wrong dep entirely. NetTcpBinding default uses BINARY message encoder (.NET MC-NMF + MC-NBFX/NBFS dictionary tables), not XML over the wire. There is no XML envelope to parse; framing is binary records with dictionary string interning (design/30-crate-topology.md:130, :247). Evidence: src/MxAsbClient/MxAsbDataClient.cs:660–685 constructsNetTcpBinding(SecurityMode.None)with no override — default binding element isBinaryMessageEncodingBindingElement. Themxaccess-asb-soapcrate name itself is a misnomer; needs MC-NMF/MC-NBFX framing, not SOAP/XML.quick-xmlmay still be needed for the small ASB control-plane XML payloads (request.ToXml()at AsbSystemAuthenticator.cs:79), but cannot frame net.tcp. - [RESOLVED]
[BLOCKER]mxaccess-asb-soapispublish = falsewhilemxaccess-asb(publishable) depends on it (design/30-crate-topology.md:128–138). Cargo refusescargo publishon a crate whose path-dep lacks a published version. Either publish both, or fold the framing module intomxaccess-asb. - [RESOLVED]
[BLOCKER]rc4 = "0.1"does not match crates.io. Latest isrc4 v0.2.0and the 0.1 line is unmaintained (design/30-crate-topology.md:245). Worse:rc4is published by RustCrypto but flagged stale; for NTLMv2 seal/sign most projects pullcipher+ a manual ARC4 or usentlm-rs/equivalent. Also0.1predates thecipher0.4 trait reform thataes 0.8/hmac 0.12were built on. - [RESOLVED]
[MAJOR]Pinned crypto versions form an inconsistent generation.aes = "0.8",hmac = "0.12",md-5 = "0.10",sha-1 = "0.10",pbkdf2 = "0.12"are the oldercipher 0.4/digest 0.10line, but the design says "1.83+ stable" (design/30-crate-topology.md:241–250). Current crates pulled from index areaes 0.9,hmac 0.13,md-5 0.11,pbkdf2 0.13— all bumped todigest 0.11/cipher 0.5and requirerust-version: 1.85. Mixing 0.10/0.12-line traits with 0.13-line will fail to resolve a coherentdigest. Either pin the whole RustCrypto generation to one line, or bump MSRV to 1.85 to match. - [RESOLVED]
[MAJOR]sha-1 = "0.10"is explicitly deprecated by upstream: "This crate is deprecated! Use the sha1 crate instead." (design/30-crate-topology.md:243). Pinning a deprecated crate in a fresh greenfield workspace is gratuitous. - [RESOLVED]
[MAJOR]windows = "0.58"is significantly stale; current is0.62.2(design/30-crate-topology.md:253). Between 0.58 and 0.62 the COM, RPC and Cryptography modules saw breaking renames (e.g.Win32_System_Rpcsurface trimmed,Security_Cryptographyreorganised). Designing against 0.58 then "upgrading later" wastes work. Pin to 0.62.x. - [RESOLVED]
[MAJOR]tiberius = "0.12"+auth-windowsclaim.tiberius0.12.3 default features includewinauth(SSPI) only on Windows; integrated security against MSSQL via SSPI is supported, but the design listsmxaccess-galaxyas "All Rust targets (TDS works cross-platform)" while only providingauth-windowsfor integrated security (design/30-crate-topology.md:94–96, :203). On Linux, the only auth options are SQL logins orintegrated-auth-gssapi(Kerberos). Galaxy databases in practice are domain-joined Windows boxes using NTLM/Kerberos integrated auth — Linux clients won't work withoutintegrated-auth-gssapiand a configured KDC. The doc claims cross-platform without flagging this. - [RESOLVED]
[MAJOR]num-bigint = "0.4"for DH ModPow is not constant-time (design/30-crate-topology.md:251). The .NET reference usesBigInteger.ModPowwhich is also not constant-time, but the Rust port has the chance to use a constant-time bignum (crypto-bigint). Since the DH exponent is the long-lived private key (AsbSystemAuthenticator.cs:153–166), a side-channel-leakymod_expre-creates a defect, not parity. Flag as a security regression vs. an opportunity. - [RESOLVED]
[MAJOR]Crate boundary:mxaccess-galaxyis split out, but the .NET reference keepsGalaxyRepositoryTagResolver.csinsideMxNativeClientnamespace (namespace MxNativeClient;at line 4) (design/30-crate-topology.md:117–122). Splitting into a separate crate forcesmxaccess-nmx→mxaccess-galaxy, but the resolver returnsMxReferenceHandle(amxaccess-codectype) and is consumed by NMX register flows — fine, no cycle. However,mxaccess-callbackdoes NOT needmxaccess-galaxy, yet the diagram routes everything throughmxaccess-nmxwhich depends on both. Minor coupling concern: the resolver pullstiberius(heavy, native-tls/rustls/winauth) into every consumer ofmxaccess-nmx. Should be a feature-gated optional dep. - [RESOLVED]
[MAJOR]Featuredpapidefault-on for Windows undermxaccess-asb— but the design does not mention#[cfg(feature = "dpapi")]boundaries insidemxaccess-asb(design/30-crate-topology.md:139, :202). The ASB shared secret is mandatory for the DH passphrase derivation (AsbSystemAuthenticator.cs:28, :134–142). Withdpapi=offand no alternate secret source spec'd, the crate cannot authenticate at all. Either removedpapias optional, or define an explicitSecretProvidertrait that DPAPI plugs into. - [RESOLVED]
[MAJOR]clippy::unwrap_used = denyinteraction with the error model (design/30-crate-topology.md:192, design/50-error-model.md:30).Arc<str>construction from&strviaArc::<str>::from(&*s)is fine, butTypeMismatch { reference: Arc<str>, ... }formatted viathiserror#[error("... {reference} ...")]requiresArc<str>: Display, which it is — no unwrap path. Real risk: any place the codec parses a UTF-16LE name and callsString::from_utf16(...).unwrap()will trip the lint. The design needs an explicit "fallible UTF-16 decode helper" rule. Worth flagging becauseMxReferenceHandleparsing is core. - [RESOLVED]
[MAJOR]MSRV 1.83 vs. dependency versions.uuid v1features["v4", "v7"]—uuid 1.23.1requiresrust-version: 1.85.0(design/30-crate-topology.md:188, :228, :233). Same for currenttiberiusindirect deps. Pinning MSRV to 1.83 while pullinguuid = "1"(= latest) is contradictory. Either pinuuid = "=1.10"(last 1.83-compatible) or raise MSRV to 1.85. - [RESOLVED]
[MINOR]"Pinned to a recent stable Rust viarust-toolchain.toml. MSRV equals the pinned version (no separately-stated MSRV — pinning is the contract)" contradictsrust-version = "1.83"in the workspace skeleton (design/30-crate-topology.md:188, :228). Pick one policy. - [RESOLVED]
[MINOR]Build commands includecargo run --example connect-write-readbut the workspace example-target only resolves at the workspace root if a single crate owns examples (design/30-crate-topology.md:20–27, :181–183). With nine crates andexamples/at the workspace root (line 20),cargo run --examplewill fail unless examples live inside a specific crate (typicallymxaccess). - [RESOLVED]
[MINOR]License "MIT OR Apache-2.0 — to be confirmed" (design/30-crate-topology.md:226, :269).tiberiusisMIT/Apache-2.0; everything else verified is MIT-or-Apache-2.0 compatible. No GPL/AGPL contamination found in the pinned set. Risk iswindows-rsproxy/stub IDL re-emissions if any are vendored from Microsoft headers — flag for legal review when the codec ports OBJREF/OXID structs derived from MIDL output. - [RESOLVED]
[NIT]"Edition 2021 initially. Edition 2024 once stable across the dependency graph" (design/30-crate-topology.md:189). Edition 2024 has been stable since Rust 1.85 (2025-02). Given pinned deps already require 1.85, just go edition 2024.
design/40-protocol-invariants.md
- [RESOLVED]
[BLOCKER]Reference registration request prefix is severely under-documented. Doc shows prefixcmd(1) + version(2) + itemHandle(i32) + correlation(GUID 16) + (-1 i16) + reservedByte + (1 i32) + itemDefinition…(design/40-protocol-invariants.md:172-180). SourceNmxReferenceRegistrationMessage.cs:15definesHeaderLength = 55, with explicit writes only at offsets 0,1,3,7,23,27 (src/MxNativeCodec/NmxReferenceRegistrationMessage.cs:80-87). Bytes 25-26 and 31-55 (≈26 bytes) are zero-initialized but never described. CLAUDE.md says "preserve unknown bytes"; the doc elides them. Rust port reading the spec will encode a 30-byte prefix instead of 55. - [RESOLVED]
[BLOCKER]Write body common prefix is wrong. Doc (design/40-protocol-invariants.md:108) says "cmd(1) + version(2) + padding(2) + handle_projection(14)" and locates wireKind at offset 17. SourceNmxWriteMessage.cs:11-13hasHandleProjectionOffset = 3,HandleProjectionLength = 14,KindOffset = 17— so layout iscmd(1) + version(2) + handle_projection(14), no 2-byte padding. The 14 bytes are at offsets 3..17, leaving wireKind at 17. Doc inserts a phantom 2-byte padding. - [RESOLVED]
[BLOCKER]String/DateTime write total size is wrong. Doc (design/40-protocol-invariants.md:118-119) says total = "26 + N".NmxWriteMessage.cs:150computesKindOffset(17) + 1 + 4 + 4 + N + 14 + 4 = 44 + N. The 26+N figure is off by 18 bytes. Anyone implementing to spec will under-allocate. - [RESOLVED]
[BLOCKER]Subscription array header offset width disagrees with the write encoder. Doc (design/40-protocol-invariants.md:120) says array layout iscount(u16) + width(u16) + 2N + suffix. Encoder agrees:NmxWriteMessage.cs:181-182writes count u16 at body[22], elementWidth u16 at body[24]. But the decoder atNmxSubscriptionMessage.cs:264-265readscountas u16 at body+4 andelementWidthas i32 at body+6. Either the encoder or decoder is wrong, and the doc only captures one shape. This is a load-bearing inconsistency that the BoM doc must resolve, not paper over. - [RESOLVED]
[BLOCKER]Boolean write value section description is wrong. Doc (design/40-protocol-invariants.md:114) says "1 byte (0xFF/0x00) + 3 reserved bytes".NmxWriteMessage.cs:257encodes[0xff, 0xff, 0xff, 0x00](true) or[0x00, 0xff, 0xff, 0x00](false) — bytes 1 and 2 are 0xFF, not "reserved". Reserved implies don't-care; native sets them to 0xFF. CLAUDE.md mandates preserving unknown bytes; calling them "reserved" invites zeroing. - [RESOLVED]
[BLOCKER]AdviseSupervisory and Advise share opcode 0x1f but the parser rejects plain Advise. Doc (design/40-protocol-invariants.md:97-99) lists three commands:Advise (plain) 0x1f / 37 bytes,AdviseSupervisory 0x1f / 39 bytes,UnAdvise 0x21 / 37 bytes. ButNmxItemControlMessage.cs:46-49rejectscommand is not (AdviseSupervisory or UnAdvise)— a 37-byte 0x1f message fails to parse. Either the doc must remove "plain Advise" or call out that the codec emits/accepts only the supervisory form (39 bytes). The duplicate enum entriesAdvise = 0x1f, AdviseSupervisory = 0x1fin the source signal this is unresolved (NmxItemControlMessage.cs:7-8). - [RESOLVED]
[MAJOR]recordCount != 1invariant for DataUpdate is missing.NmxSubscriptionMessage.cs:71-74hard-throws ifrecordCount != 1for the 0x33 DataUpdate frame. The doc treatsrecordCount(i32, typically 1)as a casual hint (design/40-protocol-invariants.md:161) and only mentions multi-record as "not yet wire-proven" (design/40-protocol-invariants.md:303). For a bill-of-materials this is an enforced invariant, not a soft expectation. The Rust port must replicate the throw to match parity. - [RESOLVED]
[MAJOR]HRESULT 0x8007139F meaning conflicts across docs and elides the canonical name. Doc (design/40-protocol-invariants.md:272) says "Uninitialized object".design/50-error-model.md:133maps it toEngineNotRegistered.docs/Capture-Run-2026-04-25.md:888anddocs/MXAccess-Public-API.md:326document it asERROR_INVALID_STATE, observed fromProcessActivateSuspend2. The 40-doc cites neither file and gives a folkloric description. - [RESOLVED]
[MAJOR]Write2 timestamped suffix description is incoherent. Doc (design/40-protocol-invariants.md:131-132): "8-byte FILETIME inserted between offsets 12 and 19 of the suffix region".NmxWriteMessage.cs:240-251WriteTimestampedSuffixactually replaces the 8-byte filler at suffix offsets 2..10 with the FILETIME (and changes the leading i16 from-1to0). The "between 12 and 19" wording does not match any offset in the source. - [RESOLVED]
[MAJOR]tailvalue for item-control is not specified.NmxItemControlMessage.cs:88defaultstail = 3for advise/unadvise. The doc only describes shape (tail(4)) and never names the constant or its value (design/40-protocol-invariants.md:102). The Rust port will pick a different value and fail at the responding NMX. This is the kind of byte-exactness this BoM is supposed to nail down. - [RESOLVED]
[MAJOR]Envelope ProtocolMarker described as0x0201int32 — but bytes 38-41 hold the LE u32. Doc (design/40-protocol-invariants.md:67) calls offset 38..42 a single i32 =0x0201.NmxTransferEnvelope.cs:99writesProtocolMarker = 0x0201as Int32LE at offset 38. So bytes are01 02 00 00. The doc's claim is technically OK but the BoM should call out the wire-byte sequence to prevent endianness mistakes, since the value visually reads as a high byte 0x02. Minor relative to others but worth noting. - [RESOLVED]
[MAJOR]Reserved offset 6 in the envelope is described as "preserved from observed (default 0)" butParsedoes not retain it. Doc (design/40-protocol-invariants.md:59) promises round-trip, butNmxTransferEnvelope.cs:39-75reads only Version, InnerLength, ProtocolMarker, etc., and discards bytes 6..10 entirely. The Encode path always writes 0 (line 91). This violates CLAUDE.md "preserve unknown bytes" and the doc misrepresents the implementation. - [RESOLVED]
[MAJOR]DCE/RPC bind UUID and active-interface UUID lack file:line citations. Two of the most load-bearing values for activation (design/40-protocol-invariants.md:13-14) cite "docs/Loopback-Protocol-Findings.md" with no line number, while every other COM identifier in the same table has a:Ncitation. For a BoM this is a missing receipt. - [RESOLVED]
[MAJOR]CRC-16 attribute signature initial value is "0" but the doc never references the spot inMxReferenceHandle.csthat proves0(vs e.g.0xFFFF). Doc (design/40-protocol-invariants.md:90) asserts initial value 0.MxReferenceHandle.cs:51does start withushort crc = 0, so the claim is correct but the byte order step (low byte then high byte of UTF-16LE per char) is shown at lines 53-56 — these need explicit:LINEanchors, not just a range. Lines 108-119 are the inner CRC step, not the per-char loop. - [RESOLVED]
[MAJOR]Status-detail table omits Source field and theDetailisshortnot byte. Doc (design/40-protocol-invariants.md:279-291) lists detail codes as bare integers.MxStatus.cs:32declaresDetailasshort(signed 16-bit). The two callbacks (DataUpdate qualityu16, status recordsi32) use different widths (NmxSubscriptionMessage.cs:126,132,136). Whether the wire is i32 (record status) or short (MxStatus.Detail) matters for sign extension on detail 8017. Doc collapses both. - [RESOLVED]
[MINOR]DataUpdate record layout in doc claimsquality(u16) + timestamp_filetime(i64) + wireKind(u8) + value(N)after status — confirmed byNmxSubscriptionMessage.cs:126-143forhasDetailStatus=false. SubscriptionStatus claimsstatus(i32) + detailStatus(i32) + quality(u16) + timestamp + wireKind + value, also confirmed. But DataUpdate header iscmd(1) + version(2) + recordCount(4) + operationId(16) = 23 bytesthen records. SubscriptionStatus header is the same 23 bytes plus a per-message correlationId(16) at offset 23, records start at 39 (NmxSubscriptionMessage.cs:98-99). Doc shows the correlationId for SubscriptionStatus only on line 153, which is correct, but the BoM table lacks an explicit "header length 23 vs 39" which would help an implementer. NIT level since the byte math is correct. - [RESOLVED]
[MINOR]Boolean array on the wire uses i16 elements (-1/0), not bool bytes.NmxWriteMessage.cs:307writes(short)-1for true.NmxSubscriptionMessage.cs:282decodes width=sizeof(short). Doc (design/40-protocol-invariants.md:120) shows2Nfor BoolArray which is consistent but doesn't say the encoding is0xFFFF/0x0000two's complement. Worth noting. - [RESOLVED]
[MINOR]RegisterEngineopnum 3 has the same signature asINmxService::RegisterEngine— but theINmxService2interface re-declaresnewversions of all base methods (NmxComContracts.cs:55-73). This means COM proxy/stub forINmxService2exposes its own opnum table starting at 3, not inheriting opnums fromINmxService. Doc (design/40-protocol-invariants.md:19) says "Opnums are sequential across the inheritance" — strictly speaking, withnewdeclarations the C# vtable has its own slots. This needs a sentence saying "in the IDL these are sequential becauseINmxService2 : INmxService, but in the .NET interop they are re-declarednew". Otherwise the Rust port may misinterpret what is bound. - [RESOLVED]
[NIT]Reference Result message tail described as "tail(16 zero)" (design/40-protocol-invariants.md:191).NmxReferenceRegistrationResultMessage.csnot read but per CLAUDE.md should preserve verbatim — doc claim "all zero" needs a Frida-capture citation, not assertion.
design/50-error-model.md
- [RESOLVED]
[BLOCKER]0x80004021mapping is fabricated for "regular operations" (design/50-error-model.md:130). The .NET reference NEVER emits0x80004021from a non-secured path — it only appears inMxNativeSession.WriteSecuredAsyncas the explanation for theNotSupportedExceptionthrown locally before any wire call (src/MxNativeClient/MxNativeSession.cs:219-220). The "regular operation" stale-handle code observed across all captures is0x80070057E_INVALIDARG (e.g. captures/probe-add-remove.log:7, captures/008-write-test-int-same-value/harness.log:7, plus theLmxProxy.dlldecompile returns0x80070057for stale handles at analysis/ghidra/exports/LmxProxy.dll.item-helper-decompile.md:60,75,88,164). TheStaleItemarm of the split is unsupported by evidence; the design even contradicts itself one row below by mapping0x80070057toInvalidArgument. Pick one. - [RESOLVED]
[BLOCKER]WriteSecuredForbiddencannot ever be returned by the Rust port as designed (design/50-error-model.md:101,129). The .NET reference does not surface0x80004021from a wire response —WriteSecuredAsyncnever reaches the wire, itthrow new NotSupportedException(...)(src/MxNativeClient/MxNativeSession.cs:218-221). For Rust parity this should map toError::Unsupported(...)at the API boundary, not a runtime HRESULT translation. TheSecurityError::AccessDenied { detail }path (design/50-error-model.md:115) is also unreachable through the proven stack — captures 111/112 show the same0x80004021even after auth changes (captures/112-frida-write-secured-auth-verified-protectedvalue1/frida-events.tsv:69). - [RESOLVED]
[MAJOR]0x800706BAis not transient in the .NET reference —is_retryable=trueis wrong (design/50-error-model.md:135,200). Every observed instance is a structural callback-marshalling failure, not a flapping NmxSvc: analysis/proxy/managed-registerengine2-callback-probe.txt:8, …-loopback-probe.txt:8, …-fixed-port-probe.txt:8 and docs/NMX-COM-Contracts.md:592 all describe it as the "no SYN to advertised port" outcome after security bindings are added — a config bug, not a retryable transient. Retrying loops forever. - [RESOLVED]
[MAJOR]is_retryable()forConnection(TcpConnect)(design/50-error-model.md:188) glosses overio::ErrorKind::AddrNotAvailable/HostUnreachable/ConnectionRefusedvsTimedOut. Retrying a name-not-found immediately is a hot-loop bug.ConnectionError::TcpConnectcarries theio::Error(design/50-error-model.md:59) —is_retryablemust inspectkind()likeIo(_)does on line 206, not blanket-yes. - [RESOLVED]
[MAJOR]MxStatusCategoryandMxStatusSourceare leaked intoError::Statuswithout#[non_exhaustive](design/50-error-model.md:45). They are plain Rust enums in the design but mirror short-valued .NET enums (src/MxNativeCodec/MxStatus.cs:3,17) where AVEVA could legally introduce new categories —Unknown=-1already implies open-set. Without#[non_exhaustive], downstreammatchstatements lock the API. Note alsoMxStatusSourceexposes six values (RequestingLmx…RespondingAutomationObject) — the Rust port should mirror these names exactly; the design never lists them. - [RESOLVED]
[MAJOR]Error::Status { detail: i16 }dropsMxStatus.Success(src/MxNativeCodec/MxStatus.cs:29). The .NETMxStatusis a 4-tuple(Success, Category, DetectedBy, Detail), andSuccess=-1is the documented "OK" sentinel (MxStatus.cs:36-58). The Rust error model loses the Success short, breaking byte-parity diagnostics demanded by CLAUDE.md ("Preserve unknown bytes"). - [RESOLVED]
[MAJOR]ConfigurationErrorcategory is non-retryable per the recovery table (design/50-error-model.md:201), but detail=21 ("Invalid reference", src/MxNativeCodec/MxStatus.cs:76) is a cold-cache miss that becomes valid after Galaxy resolver refresh — the .NET path retries by re-resolving (MxNativeSession.ResolveTagAsyncis called every operation, src/MxNativeClient/MxNativeSession.cs:173,196,232,255). Mapping the entireConfigurationErrorcategory tois_retryable=falseis too coarse. - [RESOLVED]
[MAJOR]MxValueKindDebugderive is not guaranteed by the codec (design/50-error-model.md:30). The .NET enumMxValueKind(src/MxNativeCodec/MxValueKind.cs:3-18) is the spec; the Rust port must explicitly#[derive(Debug)]and the design'sexpected:?, actual:?format (line 29) requires it. Not a blocker if obeyed, but the design must state it — neither10-raw-layer.mdnor50-error-model.mdconstrain the codec to deriveDebug. - [RESOLVED]
[MAJOR]RpcErroris exposed in the publicConnectionError::OxidResolve { source: RpcError }(design/50-error-model.md:61) butRpcErroris defined only in the raw layer (design/10-raw-layer.md:282-285) with no documentedstd::error::Errorimpl, noDisplay, and no#[non_exhaustive]declaration cited.#[source]requiresstd::error::Error. The design must promoteRpcErrorto a public, stable,Error-implementing type or wrap it. - [RESOLVED]
[MINOR]Cancellation policy onUnAdvisefailure is unspecified (design/50-error-model.md:145). "Best-effort" with no statement about whether the failure is logged, traced, or silently dropped. The .NET reference logs tomx.unadvise.error(captures/probe-add-remove.log:7) — the Rust port should commit totracing::warn!and say so. - [RESOLVED]
[MINOR]Panic policy is incomplete (design/50-error-model.md:154-159).clippy::panic = denydoes not coverunreachable!(),todo!(), indexing panics (a[i]), arithmetic overflow panics, or slice bounds. Addclippy::indexing_slicing,clippy::unreachable,clippy::todo,clippy::arithmetic_side_effects(or document why omitted). Test override "via#[cfg(test)]" is vague —#![cfg_attr(test, allow(clippy::unwrap_used))]is the actual pattern. - [RESOLVED]
[NIT]0x8001011DCallbackObjRefRejectedmapping is supported but the cite is thin: it appears only in probe-narrative docs (docs/NMX-COM-Contracts.md:591), not as a logged HRESULT incaptures/. The design should cite docs/NMX-COM-Contracts.md:590-594 directly so future maintainers can find it.
design/60-roadmap.md
- [RESOLVED]
[BLOCKER]M1 DoD claims "every Frida-captured write/advise/subscribe body incaptures/0NN-frida-*round-trips byte-identical" but several captures contain unresolved native behaviour the .NET reference itself does not yet decode (design/60-roadmap.md:35). Evidence: captures/079-082, 094 are buffered-advise scenarios where work_remain.md:177-181 says the codec layout for buffered batches is unproven (provider only emits single-sample batches), and 70-risks-and-open-questions.md:21-26 (R2) explicitly defers buffered batch parity. 036 (single-tokenWriteSecured) returns0x80004021with no payload sent (R6, line 53-58). 077-078 (suspend/activate) trigger conditions are unknown (R5). Round-trip-against-themselves is achievable, but "byte-identical to the .NET reference's encode" is not, because the reference does not encode these flows. - [RESOLVED]
[BLOCKER]M5 DoD requires the ASB type matrix to coverBoolean, Int32, Float, Double, String, DateTime, Duration, and the corresponding arrays"matchingwork_remain.md:108-113" — but work_remain.md:109 only proves "deployed array tags," not all eight scalar arrays (design/60-roadmap.md:71). The DoD over-states what the .NET reference has actually proven; less-common ASB types are explicitly deferred ("Remaining work is adding less common ASB types only as needed"). Citing the line inwork_remain.mdas authority for a stronger claim than the file makes is a dependency on unproven behaviour. - [RESOLVED]
[BLOCKER]M6 DoD "cargo benchshows codec encode/decode latency comparable to or faster than the .NET reference" directly contradicts the explicit V1 non-goal "cargo benchnumbers as gating criteria. We measure but don't gate beyond M6's loose acceptance bar." (design/60-roadmap.md:82 vs 60-roadmap.md:149). Also, "comparable to or faster than" has no defined comparison harness — the .NET reference has no microbench project. The DoD is unmeasurable as written. Same milestone also asserts "live subscribe under churn does not allocate per-message" with no allocation-counting tooling specified (compare to 70-risks-and-open-questions.md:102-108 R12 which only aims for "< 5 allocations per write at steady state"). - [RESOLVED]
[MAJOR]M2 DoD "non-zeropartnerVersionagainst a running AVEVA install" is boolean-trivial and re-proves what docs/DotNet10-Native-Library-Plan.md:64-73 already established (partner_version=6) (design/60-roadmap.md:43). It does not exerciseRegisterEngine2, callback OBJREF export, or actually receiving a status frame, despite the prose two lines above (line 41) listing those as in-scope. The DoD is weaker than what M2 promises to deliver and cannot detect regressions in the callback exporter — which is the hardest part of M2 per the .NET evidence (MxNativeClienthad to hand-rollINmxSvcCallback/IRemUnknown). - [RESOLVED]
[MAJOR]Sequencing claim "M5 can run in parallel with M3/M4" is partially false (design/60-roadmap.md:142,145). M5 saysmxaccess::Session over AsbTransport(line 68) — theSessiontype, recovery policy,Stream<Item = DataChange>, andTransporttrait are all defined in M4 (lines 55-60). M5 cannot land its public surface before M4 lands the Session abstraction. The "ASB does not depend on DCE/RPC" justification only covers the transport, not the sharedSession. This contradicts 30-crate-topology.md:64-65 wheremxaccess(top-level) sits below both transports. - [RESOLVED]
[MAJOR]Cross-implementation parity assumes the .NET reference is correct, but work_remain.md:170-174 flags the completion-only byte mapping (0x00/0x41/0xEF) as unmapped andMXSTATUS_PROXY[]conversion as missing (design/60-roadmap.md:96-102). Any Rust port that "matches the .NET reference" inherits the same gap — passing parity tests does not mean correct behaviour. The roadmap should mark these as "preserved verbatim" rather than green-checked by parity. R3/R4 acknowledge this but the validation strategy section does not. - [RESOLVED]
[MAJOR]Cross-platform drift: roadmap line 153 says "Linux is a stretch goal sitting behind feature flags" but 30-crate-topology.md:91 claimsmxaccess-galaxyis "All Rust targets (TDS works cross-platform viatiberius)" and the codec is also "All Rust targets" (line 82), and 70-risks-and-open-questions.md:128-134 Q3 says "Linux-best-effort." Three documents, three different positions. The roadmap is the most restrictive; either it is wrong or the topology over-promises. - [RESOLVED]
[MAJOR]Live-probe CI story is unspecified. M2/M3/M4/M5 DoDs all require a "running AVEVA install" + Galaxy DB +MX_LIVEenv (60-roadmap.md:43,51,62,71,93-94). There is no description of how this CI lane is provisioned — AVEVA System Platform is a licensed Windows-only install with a SQL Galaxy. Without a hosted runner story, every milestone's DoD reduces to "the author ran it once on their workstation." This is not a regression-detecting test line. - [RESOLVED]
[MINOR]M3 DoD citescaptures/022-frida-int-write*andcaptures/077-frida-advise*for byte-identical comparison (60-roadmap.md:51), but077is077-frida-suspend-advised-scanstate(per the directory listing) — a suspend capture, not an advise capture. The advise/subscribe captures are058-060,077actually exercises the unproven Suspend path (R5). Citation appears wrong or aspirational. - [RESOLVED]
[MINOR]M0 DoD includescargo publish --dry-run -p mxaccess-codec(60-roadmap.md:16) but 30-crate-topology.md:269 and Q2 (70-risks-and-open-questions.md:120-126) flag that the license is unconfirmed and there is no LICENSE in the repo.cargo publish --dry-runwill refuse withoutlicense+license-fileresolved. M0 cannot complete cleanly until Q2 is settled, but the dependency table does not reference Q2. - [RESOLVED]
[NIT]tests/fixtures/populated fromcaptures/via "junction or copy" (60-roadmap.md:12, 30-crate-topology.md:29) — junctions on Windows do not survivegit cloneon Linux/macOS, breaking the cross-platform stretch goal at the fixture-loading layer. Pick one.
design/70-risks-and-open-questions.md
- [RESOLVED]
[BLOCKER]mxaccess-asb-soapframing risk is mis-scoped — the .NET reference usesNetTcpBindingwith default BinaryMessageEncoder (.NET Binary XML / NBFX / [MC-NBFX]+[MC-NBFS]), NOT SOAP/XML. R1 talks about hand-rolling [MS-NMF] (~1500 LoC) but never lists implementing the binary dictionary/string-table codec, and 30-crate-topology.md:130 pinsquick-xmlfor an XML parse path that does not exist on the wire. The risk register should record "no XML on the wire — must implement [MC-NBFX] decoder;quick-xmlis the wrong dep" as its own R-item (design/70-risks-and-open-questions.md:7-18). Evidence: src/MxAsbClient/MxAsbDataClient.cs:663new NetTcpBinding(SecurityMode.None)with no message-encoding override → WCF defaults to binary; design/30-crate-topology.md:130 listsquick-xml. Currently missing. - [RESOLVED]
[BLOCKER]recordCount != 1 → throwinvariant has no risk entry. src/MxNativeCodec/NmxSubscriptionMessage.cs:71-74 hard-rejects any DataUpdate with more than one record. R2 is about the missing fixture, but the much bigger risk — that the codec will panic in production the first time AVEVA emits a multi-record0x33— is unrecorded. Either lift the constraint defensively or add this as its own R-item with a "behavior on rejection" plan (design/70-risks-and-open-questions.md:20-26). Evidence: src/MxNativeCodec/NmxSubscriptionMessage.cs:71-74; design/40-protocol-invariants.md:303 acknowledges the gap but only as a fixture problem. - [RESOLVED]
[BLOCKER]0x80004021 → StaleItemmapping in 50-error-model.md:130 is unevidenced. R6 admits "the reason is unknown" for secured writes, then the error model casually maps the same HRESULT on regular operations to a brand-newStaleItemenum that no capture, decompile, or .NET reference produces. This is invented protocol behavior — directly violates CLAUDE.md "do not fabricate protocol behavior" and is not flagged (design/70-risks-and-open-questions.md:52-58). Evidence: design/50-error-model.md:130 —StaleItemsynthesised; no fixture cited; design/40-protocol-invariants.md:270 only documents the secured-write path. - [RESOLVED]
[MAJOR]R3/R4 "Settles when" depends on Ghidra outputanalysis/ghidra/aaDCT tablesthat the doc itself admits is "currently absent." work_remain.md:173-174 reports "Current available decompiled/Ghidra outputs did not expose a mapping table for completion-only bytes." There is no plan, owner, or alternate path (e.g. native callback frida capture mentioned in work_remain.md:170). This is indefinitely punted — should be relabeled "indefinitely deferred" like the OperationComplete row in the evidence-gaps table, and stripped of fake settle criteria (design/70-risks-and-open-questions.md:34, 42). - [RESOLVED]
[MAJOR]No risk forDrop-time async cleanup hazards. Q6 coversClonesemantics but the design at 20-async-layer.md:70 and 50-error-model.md:145-146 openly admits drop firestokio::spawnforUnAdvise/UnregisterEngine— that requires a Tokio runtime to be alive at drop time. Drop in a sync context (e.g. final teardown afterRuntime::shutdown_timeout) will silently leak the unadvise/unregister and leak server-side handles in NMX. This is a correctness hazard and not in R1–R12 (design/70-risks-and-open-questions.md:152-158). Evidence: design/00-overview.md:38 "No spawn from inside Drop" directly contradicts design/20-async-layer.md:70 and 50-error-model.md:145. - [RESOLVED]
[MAJOR]Crypto/auth crate version-drift risk is missing. 30-crate-topology.md:130 pinsrc4,sha-1,md-5,num-bigint— all of which are at minimum-maintenance / deprecation watchlist (rc4is yanked-adjacent,sha-1v0.10 is the last RustCrypto release with a deprecation warning). Combined with 10-raw-layer.md:252 "Do not pullring— hand-roll MD4," the auth surface area depends on ~5 marginal crates. No R-item tracks "what happens when one of these is yanked / advisory'd in CI." Currently missing. - [RESOLVED]
[MAJOR]R1 and R12 have inverted severity. R1 is "hand-roll [MS-NMF] reliable-session, NBFX/NBFS dictionary codec, DH key agreement (~1500 LoC, plus the entire WCF binary message encoder you forgot)" — that's the entire ASB data plane. R12 isBytesMut::with_capacitymicro-optimization. Treating them as peer entries in an unsorted list misrepresents the project's blocker surface. The doc has no severity tier; introduce one or reorder (design/70-risks-and-open-questions.md:7, 102). - [RESOLVED]
[MAJOR]Q1 says "siblingrust/" but no workspace exists.Globofrust/confirms zero files; CLAUDE.md itself hedges ("when it is started"). The "best answer" is presented as confirmed but is in fact still a question — and M0 cannot start without it. Either escalate to an explicit decision item with an owner, or stop calling it answered (design/70-risks-and-open-questions.md:112-118). - [RESOLVED]
[MINOR]Q3 contradicts crate topology. Q3 says "Linux-best-effort … macOS unsupported in V1." 00-overview.md:35 describes "Windows-first, cross-platform-aware" with ASB SOAP framing portable. 30-crate-topology.md:130 lists nocfg(windows)gating onmxaccess-asb-soapdeps. So either the soap crate compiles on Linux (contradicting Q3 best-effort) or it doesn't (contradicting the topology). Resolve in one place (design/70-risks-and-open-questions.md:128-134). - [RESOLVED]
[MINOR]"No risk: NDR-bridge" and "No risk: custom TLB" are asserted with docs/Transport-Correlation.md and "the .NET reference" as citations but no:linenumbers and no Ghidra/capture artifact. CLAUDE.md mandates evidence; the section that proudly declares non-issues is the only section in the file with weaker citations than the risks themselves (design/70-risks-and-open-questions.md:175-187). - [RESOLVED]
[MINOR]Evidence-gaps table omits two fixtures present in work_remain.md: (a) source-level no-communication evidence (work_remain.md:198) and (b) live partial-cleanup behavior after channel failure (work_remain.md:196-197). Both are open in work_remain.md but missing from the gap table (design/70-risks-and-open-questions.md:164-171).