[R3 + R4 + R8] settle protocol-level risks per Ghidra evidence

Ghidra headless decompile of `Lmx.dll`'s `aaDCT` symbol + the
`LmxProxy.dll` Fire_* event handlers (logs at
`analysis/ghidra/exports/Lmx.dll.aadct-decompile.md` and
`analysis/ghidra/exports/LmxProxy.dll.completion-status-decompile.md`)
settles **R3** and **R4** as "no static byte→status lookup table
exists":

- `Lmx.aaDCT` at `0x10178fc0` is a `SysAllocString(L"Lmx.aaDCT")` into
  a global BSTR — a logging category name, not a table.
- `MXSTATUS_PROXY` is a 4-field struct (success/category/detectedBy/
  detail), used as the marshalled COM event payload — not a static
  array of pre-mapped statuses.
- `Fire_OnDataChange` / `Fire_OnWriteComplete` / `Fire_OperationComplete` /
  `Fire_OnBufferedDataChange` (RVAs 0x15f72, 0x1611f, 0x16271, 0x163c0
  in `LmxProxy.dll`) 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-event computation from operation context (engine
  ids, item handles, retry counters), not a static promotion.

R3/R4 status updated from "indefinitely deferred — no Ghidra table"
to "settled — no table exists; verbatim preservation is the canonical
answer." The .NET reference's `NmxOperationStatusMessage.TryParseInner`
+ the Rust port's `mxaccess-codec/src/operation_status.rs` already
match this canonical behaviour; no code change required.

Reopen R3/R4 only if a context-aware capture surfaces a per-byte
synthesis logic that depends on operation context — at which point
the codec would need access to the originating operation's context,
which is upstream of the bytes themselves.

**R8** marked permanently deferred — implementation already parses
NTLM AV pairs per [MS-NLMP] §2.2.2.1 (including the cross-domain
shapes `MsvAvDnsTreeName` / `MsvAvDnsComputerName` carrying the
trusted-domain DNS suffix), what's missing is the live capture, and
the live capture requires a multi-domain Windows lab not available
on this dev host. Same status pattern as F3 in `design/followups.md`.

Open evidence gaps table updated to reflect:
- Cross-domain NTLM: deferred (R8)
- Ghidra mapping table for completion-only bytes: no table exists
  (R3/R4 settled)
- Activate/Suspend transition (wire): partial (F44 + F46), live re-run
  pending (F50)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-06 06:23:05 -04:00
parent 0e93e3a8fa
commit 4dfc0cee65
3 changed files with 508 additions and 17 deletions
@@ -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;
}
```
@@ -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<wchar_t,struct_std::char_traits<wchar_t>_> bVar1;
undefined4 *puVar2;
int *piVar3;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *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<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_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 *)&param_3) {
ATL::CComVariant::InternalCopy((CComVariant *)(this_00 + 0xc),(tagVARIANT *)&param_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<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 0x10));
if (bVar1 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)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<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 0x10),
L"CProxy_ILMXProxyServerEvents::Fire_OnDataChange firing event - Server Handle "
);
uVar11 = (ushort)uVar12;
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar5);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar6);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar7);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar8);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar9);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar10);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,uVar11);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::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<wchar_t,struct_std::char_traits<wchar_t>_> bVar1;
undefined4 *puVar2;
int *piVar3;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar4;
undefined4 *this_00;
long lVar5;
wchar_t *pwVar6;
long lVar7;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_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<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 0xc));
if (bVar1 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
pwVar6 = L" Item Handle ";
lVar5 = param_1;
lVar7 = param_2;
p_Var8 = endl_exref;
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 0xc),
L"CProxy_ILMXProxyServerEvents::Fire_OnWriteComplete firing event - Server Handle "
);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar5);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar6);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar7);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::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<wchar_t,struct_std::char_traits<wchar_t>_> bVar1;
undefined4 *puVar2;
int *piVar3;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar4;
undefined4 *this_00;
long lVar5;
wchar_t *pwVar6;
long lVar7;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_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<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 0xc));
if (bVar1 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
pwVar6 = L" Item Handle ";
lVar5 = param_1;
lVar7 = param_2;
p_Var8 = endl_exref;
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 0xc),
L"CProxy_ILMXProxyServerEvents::Fire_OperationComplete firing event - Server Handle "
);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar5);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar6);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar7);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::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<wchar_t,struct_std::char_traits<wchar_t>_> bVar1;
undefined4 *puVar2;
int *piVar3;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar4;
undefined4 *this_00;
undefined4 in_stack_00000040;
long lVar5;
wchar_t *pwVar6;
long lVar7;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_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<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 0x10));
if (bVar1 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
pwVar6 = L" Item Handle ";
lVar5 = param_1;
lVar7 = param_2;
p_Var8 = endl_exref;
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 0x10),
L"CProxy_ILMXProxyServerEvents2::Fire_OnBufferedDataChange firing event - Server Handle "
);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar5);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar6);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar7);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::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;
}
```
+25 -17
View File
@@ -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:154163`: 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<Item = RawOperationStatus>` 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 bytestatus 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<Item = RawOperationStatus>` 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:164174`). 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:164174`) 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 |