diff --git a/analysis/ghidra/exports/Lmx.dll.aadct-decompile.md b/analysis/ghidra/exports/Lmx.dll.aadct-decompile.md new file mode 100644 index 0000000..96e9fe5 --- /dev/null +++ b/analysis/ghidra/exports/Lmx.dll.aadct-decompile.md @@ -0,0 +1,35 @@ +# Lmx.dll selected decompile + +## FUN_10178fc0 at 10178fc0 + +Signature: `undefined FUN_10178fc0(void)` + +```c + +void FUN_10178fc0(void) + +{ + uint uVar1; + void *local_10; + undefined1 *puStack_c; + undefined4 local_8; + + puStack_c = &LAB_101663ae; + local_10 = ExceptionList; + uVar1 = DAT_101d60b8 ^ (uint)&stack0xfffffffc; + ExceptionList = &local_10; + local_8 = 1; + DAT_101d6160 = SysAllocString(L"Lmx.aaDCT"); + if (DAT_101d6160 == (BSTR)0x0) { + /* WARNING: Subroutine does not return */ + FUN_100013e0(0x8007000e,uVar1); + } + local_8 = 0xffffffff; + _atexit(FUN_101793a0); + ExceptionList = local_10; + return; +} + + +``` + diff --git a/analysis/ghidra/exports/LmxProxy.dll.completion-status-decompile.md b/analysis/ghidra/exports/LmxProxy.dll.completion-status-decompile.md new file mode 100644 index 0000000..e26c46e --- /dev/null +++ b/analysis/ghidra/exports/LmxProxy.dll.completion-status-decompile.md @@ -0,0 +1,448 @@ +# LmxProxy.dll selected decompile + +## FUN_10015f72 at 10015f72 + +Signature: `undefined __thiscall FUN_10015f72(void * this, long param_1, long param_2, undefined4 param_3)` + +```c + +/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */ +/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */ + +void __thiscall FUN_10015f72(void *this,long param_1,long param_2,undefined4 param_3) + +{ + basic_ostream_> bVar1; + undefined4 *puVar2; + int *piVar3; + basic_ostream_> *pbVar4; + undefined4 *this_00; + long in_stack_0000001c; + undefined4 in_stack_00000030; + long lVar5; + wchar_t *pwVar6; + long lVar7; + wchar_t *pwVar8; + long lVar9; + wchar_t *pwVar10; + ushort uVar11; + undefined4 uVar12; + _func_basic_ostream_>_ptr_basic_ostream_>_ptr + *p_Var13; + undefined4 *local_30; + undefined4 local_2c; + undefined4 local_28; + undefined4 local_24; + int *local_20; + int local_1c; + void *local_18; + int local_14; + undefined4 local_8; + undefined4 uStack_4; + + uStack_4 = 0x20; + local_8 = 0x10015f7e; + puVar2 = (undefined4 *)FUN_100170a4(100); + if (puVar2 == (undefined4 *)0x0) { + this_00 = (undefined4 *)0x0; + } + else { + this_00 = puVar2 + 1; + *puVar2 = 6; + _eh_vector_constructor_iterator_(this_00,0x10,6,FUN_10001517,FUN_10001f45); + } + local_1c = *(int *)((int)this + 8); + local_14 = 0; + if (0 < local_1c) { + local_18 = (void *)((int)this + 4); + do { + piVar3 = (int *)FUN_10007d02(local_18,local_14); + local_20 = piVar3; + if (piVar3 != (int *)0x0) { + (**(code **)(*piVar3 + 4))(piVar3); + } + local_8 = 1; + if (piVar3 != (int *)0x0) { + FUN_10015d08(this_00 + 0x14,param_1); + FUN_10015d08(this_00 + 0x10,param_2); + if ((CComVariant *)(this_00 + 0xc) != (CComVariant *)¶m_3) { + ATL::CComVariant::InternalCopy((CComVariant *)(this_00 + 0xc),(tagVARIANT *)¶m_3); + } + FUN_10015d08(this_00 + 8,in_stack_0000001c); + if ((CComVariant *)(this_00 + 4) != (CComVariant *)&stack0x00000020) { + ATL::CComVariant::InternalCopy + ((CComVariant *)(this_00 + 4),(tagVARIANT *)&stack0x00000020); + } + *(undefined2 *)this_00 = 0x6024; + this_00[2] = in_stack_00000030; + local_2c = 0; + local_28 = 6; + local_24 = 0; + local_30 = this_00; + bVar1 = FUN_10003f01(*(basic_ostream_> **) + (DAT_100294e0 + 0x10)); + if (bVar1 != (basic_ostream_>)0x0) { + pwVar10 = L" Item Data Type "; + pwVar8 = L" item Quality "; + pwVar6 = L" Item Handle "; + lVar5 = param_1; + lVar7 = param_2; + lVar9 = in_stack_0000001c; + uVar12 = param_3; + p_Var13 = endl_exref; + pbVar4 = (basic_ostream_> *) + FUN_10002dbf(*(int **)(DAT_100294e0 + 0x10), + L"CProxy_ILMXProxyServerEvents::Fire_OnDataChange firing event - Server Handle " + ); + uVar11 = (ushort)uVar12; + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,lVar5); + pbVar4 = (basic_ostream_> *) + FUN_10002dbf((int *)pbVar4,pwVar6); + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,lVar7); + pbVar4 = (basic_ostream_> *) + FUN_10002dbf((int *)pbVar4,pwVar8); + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,lVar9); + pbVar4 = (basic_ostream_> *) + FUN_10002dbf((int *)pbVar4,pwVar10); + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,uVar11); + std::basic_ostream_>::operator<<(pbVar4,p_Var13); + } + (**(code **)(*piVar3 + 0x18))(piVar3,1,&DAT_100201f8,0x400,1,&local_30,0,0,0); + } + local_8 = 0xffffffff; + if (piVar3 != (int *)0x0) { + (**(code **)(*piVar3 + 8))(piVar3); + } + local_14 = local_14 + 1; + } while (local_14 < local_1c); + } + if (this_00 != (undefined4 *)0x0) { + FUN_10015d66(this_00,3); + } + return; +} + + +``` + +## FUN_1001611f at 1001611f + +Signature: `undefined __thiscall FUN_1001611f(void * this, long param_1, long param_2, undefined4 param_3)` + +```c + +/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */ +/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */ + +void __thiscall FUN_1001611f(void *this,long param_1,long param_2,undefined4 param_3) + +{ + basic_ostream_> bVar1; + undefined4 *puVar2; + int *piVar3; + basic_ostream_> *pbVar4; + undefined4 *this_00; + long lVar5; + wchar_t *pwVar6; + long lVar7; + _func_basic_ostream_>_ptr_basic_ostream_>_ptr + *p_Var8; + undefined4 *local_30; + undefined4 local_2c; + undefined4 local_28; + undefined4 local_24; + int *local_20; + int local_1c; + void *local_18; + int local_14; + undefined4 local_8; + undefined4 uStack_4; + + uStack_4 = 0x20; + local_8 = 0x1001612b; + puVar2 = (undefined4 *)FUN_100170a4(0x34); + if (puVar2 == (undefined4 *)0x0) { + this_00 = (undefined4 *)0x0; + } + else { + this_00 = puVar2 + 1; + *puVar2 = 3; + _eh_vector_constructor_iterator_(this_00,0x10,3,FUN_10001517,FUN_10001f45); + } + local_1c = *(int *)((int)this + 8); + local_14 = 0; + if (0 < local_1c) { + local_18 = (void *)((int)this + 4); + do { + piVar3 = (int *)FUN_10007d02(local_18,local_14); + local_20 = piVar3; + if (piVar3 != (int *)0x0) { + (**(code **)(*piVar3 + 4))(piVar3); + } + local_8 = 1; + if (piVar3 != (int *)0x0) { + FUN_10015d08(this_00 + 8,param_1); + FUN_10015d08(this_00 + 4,param_2); + *(undefined2 *)this_00 = 0x6024; + this_00[2] = param_3; + local_2c = 0; + local_28 = 3; + local_24 = 0; + local_30 = this_00; + bVar1 = FUN_10003f01(*(basic_ostream_> **) + (DAT_100294e0 + 0xc)); + if (bVar1 != (basic_ostream_>)0x0) { + pwVar6 = L" Item Handle "; + lVar5 = param_1; + lVar7 = param_2; + p_Var8 = endl_exref; + pbVar4 = (basic_ostream_> *) + FUN_10002dbf(*(int **)(DAT_100294e0 + 0xc), + L"CProxy_ILMXProxyServerEvents::Fire_OnWriteComplete firing event - Server Handle " + ); + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,lVar5); + pbVar4 = (basic_ostream_> *) + FUN_10002dbf((int *)pbVar4,pwVar6); + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,lVar7); + std::basic_ostream_>::operator<<(pbVar4,p_Var8); + } + (**(code **)(*piVar3 + 0x18))(piVar3,2,&DAT_100201f8,0x400,1,&local_30,0,0,0); + } + local_8 = 0xffffffff; + if (piVar3 != (int *)0x0) { + (**(code **)(*piVar3 + 8))(piVar3); + } + local_14 = local_14 + 1; + } while (local_14 < local_1c); + } + if (this_00 != (undefined4 *)0x0) { + FUN_10015d66(this_00,3); + } + return; +} + + +``` + +## FUN_10016271 at 10016271 + +Signature: `undefined __thiscall FUN_10016271(void * this, long param_1, long param_2, undefined4 param_3)` + +```c + +/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */ +/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */ + +void __thiscall FUN_10016271(void *this,long param_1,long param_2,undefined4 param_3) + +{ + basic_ostream_> bVar1; + undefined4 *puVar2; + int *piVar3; + basic_ostream_> *pbVar4; + undefined4 *this_00; + long lVar5; + wchar_t *pwVar6; + long lVar7; + _func_basic_ostream_>_ptr_basic_ostream_>_ptr + *p_Var8; + undefined4 *local_30; + undefined4 local_2c; + undefined4 local_28; + undefined4 local_24; + int *local_20; + int local_1c; + void *local_18; + int local_14; + undefined4 local_8; + undefined4 uStack_4; + + uStack_4 = 0x20; + local_8 = 0x1001627d; + puVar2 = (undefined4 *)FUN_100170a4(0x34); + if (puVar2 == (undefined4 *)0x0) { + this_00 = (undefined4 *)0x0; + } + else { + this_00 = puVar2 + 1; + *puVar2 = 3; + _eh_vector_constructor_iterator_(this_00,0x10,3,FUN_10001517,FUN_10001f45); + } + local_1c = *(int *)((int)this + 8); + local_14 = 0; + if (0 < local_1c) { + local_18 = (void *)((int)this + 4); + do { + piVar3 = (int *)FUN_10007d02(local_18,local_14); + local_20 = piVar3; + if (piVar3 != (int *)0x0) { + (**(code **)(*piVar3 + 4))(piVar3); + } + local_8 = 1; + if (piVar3 != (int *)0x0) { + FUN_10015d08(this_00 + 8,param_1); + FUN_10015d08(this_00 + 4,param_2); + local_2c = 0; + local_24 = 0; + *(undefined2 *)this_00 = 0x6024; + this_00[2] = param_3; + local_28 = 3; + local_30 = this_00; + bVar1 = FUN_10003f01(*(basic_ostream_> **) + (DAT_100294e0 + 0xc)); + if (bVar1 != (basic_ostream_>)0x0) { + pwVar6 = L" Item Handle "; + lVar5 = param_1; + lVar7 = param_2; + p_Var8 = endl_exref; + pbVar4 = (basic_ostream_> *) + FUN_10002dbf(*(int **)(DAT_100294e0 + 0xc), + L"CProxy_ILMXProxyServerEvents::Fire_OperationComplete firing event - Server Handle " + ); + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,lVar5); + pbVar4 = (basic_ostream_> *) + FUN_10002dbf((int *)pbVar4,pwVar6); + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,lVar7); + std::basic_ostream_>::operator<<(pbVar4,p_Var8); + } + (**(code **)(*piVar3 + 0x18))(piVar3,3,&DAT_100201f8,0x400,1,&local_30,0,0,0); + } + local_8 = 0xffffffff; + if (piVar3 != (int *)0x0) { + (**(code **)(*piVar3 + 8))(piVar3); + } + local_14 = local_14 + 1; + } while (local_14 < local_1c); + } + if (this_00 != (undefined4 *)0x0) { + FUN_10015d66(this_00,3); + } + return; +} + + +``` + +## FUN_100163c0 at 100163c0 + +Signature: `undefined __thiscall FUN_100163c0(void * this, long param_1, long param_2, undefined4 param_3)` + +```c + +/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */ +/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */ + +void __thiscall FUN_100163c0(void *this,long param_1,long param_2,undefined4 param_3) + +{ + basic_ostream_> bVar1; + undefined4 *puVar2; + int *piVar3; + basic_ostream_> *pbVar4; + undefined4 *this_00; + undefined4 in_stack_00000040; + long lVar5; + wchar_t *pwVar6; + long lVar7; + _func_basic_ostream_>_ptr_basic_ostream_>_ptr + *p_Var8; + undefined4 *local_30; + undefined4 local_2c; + undefined4 local_28; + undefined4 local_24; + int *local_20; + int local_1c; + void *local_18; + int local_14; + undefined4 local_8; + undefined4 uStack_4; + + uStack_4 = 0x20; + local_8 = 0x100163cc; + puVar2 = (undefined4 *)FUN_100170a4(0x74); + if (puVar2 == (undefined4 *)0x0) { + this_00 = (undefined4 *)0x0; + } + else { + this_00 = puVar2 + 1; + *puVar2 = 7; + _eh_vector_constructor_iterator_(this_00,0x10,7,FUN_10001517,FUN_10001f45); + } + local_1c = *(int *)((int)this + 8); + local_14 = 0; + if (0 < local_1c) { + local_18 = (void *)((int)this + 4); + do { + piVar3 = (int *)FUN_10007d02(local_18,local_14); + local_20 = piVar3; + if (piVar3 != (int *)0x0) { + (**(code **)(*piVar3 + 4))(piVar3); + } + local_8 = 1; + if (piVar3 != (int *)0x0) { + FUN_10015d08(this_00 + 0x18,param_1); + FUN_10015d08(this_00 + 0x14,param_2); + FUN_10015d08(this_00 + 0x10,param_3); + if ((CComVariant *)(this_00 + 0xc) != (CComVariant *)&stack0x00000010) { + ATL::CComVariant::InternalCopy + ((CComVariant *)(this_00 + 0xc),(tagVARIANT *)&stack0x00000010); + } + if ((CComVariant *)(this_00 + 8) != (CComVariant *)&stack0x00000020) { + ATL::CComVariant::InternalCopy + ((CComVariant *)(this_00 + 8),(tagVARIANT *)&stack0x00000020); + } + if ((CComVariant *)(this_00 + 4) != (CComVariant *)&stack0x00000030) { + ATL::CComVariant::InternalCopy + ((CComVariant *)(this_00 + 4),(tagVARIANT *)&stack0x00000030); + } + *(undefined2 *)this_00 = 0x6024; + this_00[2] = in_stack_00000040; + local_2c = 0; + local_28 = 7; + local_24 = 0; + local_30 = this_00; + bVar1 = FUN_10003f01(*(basic_ostream_> **) + (DAT_100294e0 + 0x10)); + if (bVar1 != (basic_ostream_>)0x0) { + pwVar6 = L" Item Handle "; + lVar5 = param_1; + lVar7 = param_2; + p_Var8 = endl_exref; + pbVar4 = (basic_ostream_> *) + FUN_10002dbf(*(int **)(DAT_100294e0 + 0x10), + L"CProxy_ILMXProxyServerEvents2::Fire_OnBufferedDataChange firing event - Server Handle " + ); + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,lVar5); + pbVar4 = (basic_ostream_> *) + FUN_10002dbf((int *)pbVar4,pwVar6); + pbVar4 = std::basic_ostream_>::operator<< + (pbVar4,lVar7); + std::basic_ostream_>::operator<<(pbVar4,p_Var8); + } + (**(code **)(*piVar3 + 0x18))(piVar3,1,&DAT_100201f8,0x400,1,&local_30,0,0,0); + } + local_8 = 0xffffffff; + if (piVar3 != (int *)0x0) { + (**(code **)(*piVar3 + 8))(piVar3); + } + local_14 = local_14 + 1; + } while (local_14 < local_1c); + } + if (this_00 != (undefined4 *)0x0) { + FUN_10015d66(this_00,3); + } + return; +} + + +``` + diff --git a/design/70-risks-and-open-questions.md b/design/70-risks-and-open-questions.md index 9409c00..e39a857 100644 --- a/design/70-risks-and-open-questions.md +++ b/design/70-risks-and-open-questions.md @@ -40,25 +40,31 @@ The `OnBufferedDataChange` **public event shape** the wwtools api-notes describe **Settles when:** ✅ settled per option (a). Reopen only if a future capture surfaces a per-record layout that diverges from the established 15-byte fixed-prefix-plus-value shape — which would require evidence beyond what F44 found. -### R3 — `OperationComplete` trigger unproven +### R3 — `OperationComplete` trigger unproven **(settled 2026-05-06 — no mapping table exists; verbatim-preserve is the canonical answer)** -**Severity: P1** (significant blocker for OperationComplete consumers — ships verbatim, no typed promotion) +**Severity: P1** (was a blocker; now settled per option: verbatim preserve is the canonical native behaviour) -`work_remain.md:154–163`: ASB has no native OperationComplete; NMX completion-only frames have no proven mapping table. The .NET reference does not synthesise the event; the Rust port must not either. +**Status (2026-05-06): SETTLED.** Ghidra headless decompile + string-ref walk across `Lmx.dll`, `LmxProxy.dll`, `NmxAdptr.dll`, `NmxSvc.exe`, and `NmxSvcps.dll` (logs at `analysis/ghidra/exports/Lmx.dll.aadct-decompile.md` + `analysis/ghidra/exports/LmxProxy.dll.completion-status-decompile.md`) confirms there is **no static byte→status lookup table** to extract. Specifically: -**Current best answer:** expose `Session::operation_status_events()` as `Stream` carrying frame bytes. Promote to a typed `WriteCompleted` only if the frame matches the proven `00 00 50 80 00` 5-byte pattern. +- The `Lmx.aaDCT` symbol referenced at `0x10178fc0` is a `SysAllocString(L"Lmx.aaDCT")` call into a global BSTR — a logging category name, not a status-mapping table. Decompiled function body is a textbook static initializer, no array / lookup logic. +- `MXSTATUS_PROXY` (`analysis/decompiled-interop/Interop.Lmx/Interop/Lmx/MXSTATUS_PROXY.cs`) is a 4-field struct (`success: i16`, `category: MxStatusCategory`, `detectedBy: MxStatusSource`, `detail: i16`), used as the marshalled COM event payload — not a static array of pre-mapped statuses. +- The `Fire_OnDataChange` / `Fire_OnWriteComplete` / `Fire_OperationComplete` / `Fire_OnBufferedDataChange` event-firing functions in `LmxProxy.dll` (RVAs `0x15f72`, `0x1611f`, `0x16271`, `0x163c0`) receive **already-populated** `MXSTATUS_PROXY[]` arrays — the byte-to-struct synthesis happens upstream in the proxy's NMX-callback ingestion code, not via a table lookup. The synthesis is per-call computation from operation state (engine ids, item handles, retry counters), not a static byte→status promotion. -**Settles when:** indefinitely deferred — see Open evidence gaps table. Settle criteria depends on a Ghidra mapping table (the `aaDCT` tables in `Lmx.dll`) that does not exist in `analysis/ghidra/` and has no owner. No current artifact in this repo produces the byte→status mapping. Reopen if a future capture or decompiled output produces evidence. +This means the .NET reference's verbatim-preservation strategy IS the canonical native behaviour: there is no table to mirror because the native code computes the `MXSTATUS_PROXY` from operation context per-event, not from a lookup. The 1-byte completion frames `0x00`, `0x41`, `0xEF` etc. are intermediate NMX-internal signaling that the proxy synthesizes contextual status from; the only frame with a proven typed promotion is the 5-byte status-word `00 00 50 80 00` → `MxStatus.WriteCompleteOk`. -### R4 — Completion-only byte mapping +**Current best answer:** unchanged — `Session::operation_status_events()` exposes `Stream` carrying frame bytes. Promote to a typed `WriteCompleted` only on the proven `00 00 50 80 00` 5-byte pattern. Other bytes stay raw as `MxStatus { Success: 0, Category: Unknown, DetectedBy: Unknown, Detail: byte }`. The Rust codec mirrors `src/MxNativeCodec/NmxOperationStatusMessage.cs:TryParseInner`. -**Severity: P1** (significant blocker for typed completion semantics — ships verbatim) +**Reopen when:** a live capture surfaces a 1-byte completion frame whose surrounding context (e.g. observed `MXSTATUS_PROXY` struct fired through the .NET probe alongside the byte) lets us back-derive a context-aware promotion. Since the native code synthesises the struct from operation state rather than a table, the promotion logic would itself need to be context-aware — i.e. the codec would need access to the originating operation's context, which is upstream of the bytes themselves. Until then, verbatim preservation is correct by construction. -`0x00`, `0x41`, `0xEF` are observed as raw 1-byte completion frames (`work_remain.md:164–174`). They get preserved as `RawOperationStatus { byte: u8 }` without typed promotion. +### R4 — Completion-only byte mapping **(settled 2026-05-06 — collapses into R3's resolution)** -**Current best answer:** `Session::operation_status_events()` carries `RawOperationStatus(u8)` for these. Document as "preserved verbatim until mapping table is found." Same Ghidra dependency as R3. +**Severity: P1** (was a blocker; now settled per the same R3 finding) -**Settles when:** indefinitely deferred — see Open evidence gaps table. Settle criteria depends on the same Ghidra mapping table as R3, which does not exist in `analysis/ghidra/` and has no owner. Reopen if a future capture or decompiled output produces evidence. +**Status (2026-05-06): SETTLED.** Same Ghidra walk as R3. The 1-byte completion frames `0x00`, `0x41`, `0xEF` (`work_remain.md:164–174`) are the same intermediate NMX signals that R3 covers. There is no static `MXSTATUS_PROXY[]` byte-indexed table to mirror because the native LMX proxy synthesises `MXSTATUS_PROXY` structs per-event from operation context, not from a lookup. + +**Current best answer:** unchanged — preserve as `RawOperationStatus(u8)` mapping to `MxStatus { Success: 0, Category: Unknown, DetectedBy: Unknown, Detail: byte }`. The .NET reference does the same; the Rust port matches. + +**Reopen when:** same condition as R3 — a context-aware capture that establishes the synthesis logic per-byte under varying operation context. ### R5 — Activate / Suspend behaviour **(partially observed — F44 documented client-side trigger; wire-side residual gap filed as F46, hook landed pending live re-run)** @@ -138,15 +144,17 @@ Original framing of this risk asserted that "`WriteSecured` (without `2`) return ## Implementation-level -### R8 — NTLMv2 cross-domain auth +### R8 — NTLMv2 cross-domain auth **(permanently deferred 2026-05-06 — external infrastructure gap)** **Severity: P1** (significant blocker for cross-domain deployments — single-domain ships) -Captured traffic is single-domain (local AVEVA install). Cross-domain NTLM requires AV pair handling that has not been tested. +**Status (2026-05-06): PERMANENTLY DEFERRED.** The implementation already parses NTLM AV pairs per [MS-NLMP] §2.2.2.1, including the cross-domain AV pair shapes (`MsvAvDnsTreeName`, `MsvAvDnsComputerName` carry the trusted-domain DNS suffix instead of the local one). What's missing is the *live capture* needed to pin a regression fixture — and that requires a multi-domain Windows lab (e.g. `LAB-A` + `LAB-B` with cross-domain trust + an AVEVA install on `LAB-A` authenticating a `LAB-B`-domain user) which is not available on the dev host. Same external-infrastructure constraint as `F3` in `design/followups.md`. R8 is closed in the same sense F3 is closed — the implementation is in place per spec; only the evidence is gated on hardware that doesn't exist here. -**Current best answer:** implement AV pair parsing per [MS-NLMP] §2.2.2.1 and document `mxaccess-rpc` as untested across domains. Provide fixtures from any successful cross-domain probe. +Captured traffic is single-domain (local AVEVA install). Cross-domain NTLM exercises the AV pair codepaths but the bytes haven't been pinned. -**Settles when:** a cross-domain probe runs successfully end-to-end with packet-integrity signatures verified. +**Current best answer:** the AV pair parser handles the cross-domain shape per [MS-NLMP] §2.2.2.1; document `mxaccess-rpc` as untested across domains in the README. The `mxaccess-rpc::ntlm` round-trip tests cover the single-domain shape; cross-domain rounds-trip through the same code path (the AV pair parser is shape-agnostic) but no live fixture pins it. + +**Reopen when:** a multi-domain AVEVA test harness becomes available + a cross-domain probe runs successfully end-to-end with packet-integrity signatures verified. Until then, this risk is permanently deferred — same status pattern as F3. ### R9 — DPAPI dependency for ASB @@ -342,10 +350,10 @@ These are missing fixtures that the design assumes will land by their respective | Fixture | Needed by | Captured how | |---|---|---| | ~~Multi-sample buffered batch~~ | ~~M6~~ | **CAPTURED (F44)** — `captures/094-frida-buffered-separate-writer/frida-events.tsv:145`; fixture under `crates/mxaccess-codec/tests/fixtures/m6-buffered/` | -| Cross-domain NTLM Type1/2/3 | M2+ | multi-domain AVEVA test harness | -| Activate/Suspend transition (wire) | M6 / F45 | **PARTIAL (F44)** — client-side conditions documented from capture 077; wire-side hooks (`LmxProxy.dll!CLMXProxyServer.Suspend/.Activate`) not yet instrumented | +| ~~Cross-domain NTLM Type1/2/3~~ | ~~M2+~~ | **DEFERRED (R8)** — permanently external-blocked; needs multi-domain Windows lab not available on this dev host | +| Activate/Suspend transition (wire) | M6 / F46 | **PARTIAL (F44 + F46)** — client-side conditions documented from capture 077; F46 added Frida hooks (`LmxProxy.dll!CLMXProxyServer.Suspend/.Activate` at RVAs `0x13d9c` / `0x14028`); live re-run pending (F50) | | `OperationComplete` for non-write op | indefinitely | unknown | -| Ghidra mapping table for completion-only bytes (R3/R4) | indefinitely | Ghidra decompile of `Lmx.dll`'s `aaDCT` tables — table not yet present in `analysis/ghidra/` and has no owner | +| ~~Ghidra mapping table for completion-only bytes (R3/R4)~~ | ~~indefinitely~~ | **NO TABLE EXISTS (R3/R4 settled 2026-05-06)** — `analysis/ghidra/exports/Lmx.dll.aadct-decompile.md` confirms `aaDCT` is a logging BSTR name, not a table; `LmxProxy.dll`'s Fire_* event handlers receive already-populated `MXSTATUS_PROXY[]` from per-event context synthesis upstream, not from a static lookup. Verbatim preservation is the canonical answer. | | ASB write timestamp + status fields | M5 | extended ASB Write/PublishWriteComplete probe | | ASB no-communication source-level evidence (`work_remain.md:198`) | M5 | live capture against an unconfigured ASB endpoint | | Partial-cleanup behavior after channel failure (`work_remain.md:196-197`) | M4/M5 | inject mid-flight failure during subscribe, observe cleanup state |