[F55] Path C investigation: NmxSvc requires SCM-registered OXID for callbacks
Captured OBJREF byte structures from both paths via the .NET probe: - `--probe-callback-marshal`: DCOM-marshalled, 338 bytes, succeeds (when used inside `MxNativeSession.Open` → `CreateRegisteredService`). - `--probe-register-managed-callback`: hand-rolled, 162 bytes, fails with `RegisterEngine2 → 0x800706BA RPC_S_SERVER_UNAVAILABLE`. The structural diff: - `std_flags`: DCOM=`0x0A80` (SORF_OXRES4+6+8) vs hand-rolled=`0x280` (SORF_OXRES4+6). Bit `0x0800` (SORF_OXRES8) only set in DCOM. - ncacn_ip_tcp bindings: DCOM=4 with no ports; hand-rolled=1 with explicit `[port]`. - Total size: 338 vs 162 bytes. Tested the simplest fix (hand-rolled `std_flags = 0x0A80` to match DCOM): **still fails with the same 1722.** Reverted. **Diagnosis updated in F55:** NmxSvc on receiving RegisterEngine2 appears to call `IObjectExporter::ResolveOxid` against the local SCM (`127.0.0.1:135`) to resolve the callback OBJREF's OXID, then dial the resulting bindings. Our hand-rolled OXID is never registered with RPCSS, so the SCM-side resolution fails and NmxSvc returns RPC_S_SERVER_UNAVAILABLE — matching: - the symptom (1722), - the sub-second timing (no TCP dial-back to our listener attempted), - the fact that the .NET `ManagedCallbackExporter` (same hand-rolled approach) ALSO fails identically. DCOM marshalling fixes this because `CoMarshalInterface` internally registers the OXID with RPCSS. The bindings have no port because RPCSS returns the dynamic port from the DCOM stub layer. **Conclusion: Path A is the architecturally correct fix** — the callback exporter must be a DCOM-managed object (e.g. via `windows-rs` `#[implement]`) for NmxSvc to accept the callback. The hand-rolled-listener-with-explicit-port approach is fundamentally incompatible with NmxSvc's callback validation, in both Rust and the .NET reference. Path C (cheap investigation) is exhausted; F55 verdict updated to recommend Path A explicitly. `cargo test --workspace` 824 passing; clippy `-D warnings` clean across both feature configurations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-4
@@ -111,10 +111,24 @@ Between each publish: wait for the crate to be indexed before the next one's `ca
|
||||
|
||||
**The OBJREF binding is correct:** `DESKTOP-6JL3KKO[<port>]` with `port` from `tokio::net::TcpListener::bind(0.0.0.0:0)`. Windows Firewall is OFF on all profiles. The hand-rolled exporter accepts connections; NmxSvc just refuses to use it.
|
||||
|
||||
**Hypotheses (each needs verification):**
|
||||
1. NmxSvc validates callback OBJREFs through Windows DCOM (`CoUnmarshalInterface` or similar) before registering them — and the hand-rolled blob fails that validation, surfacing as `RPC_S_SERVER_UNAVAILABLE` because COM interprets it as "the named server is unreachable".
|
||||
2. The OBJREF carries fields (e.g. specific `STDOBJREF.flags`, security bindings, or authn-hint values) NmxSvc requires that the hand-rolled builder doesn't set correctly. Comparing the byte-by-byte structure shows identical layout to .NET's hand-rolled OBJREF — but the same .NET hand-rolled OBJREF also fails. So this isn't a Rust-vs-.NET layout drift, it's an architecture-vs-NmxSvc gap.
|
||||
3. The NmxSvc version on this dev machine has stricter callback validation than the reference development version targeted by `MxNativeClient`'s original architecture. (NmxSvc release notes / version unknown at this point.)
|
||||
**Path C investigation (2026-05-06).** Captured the OBJREF byte structure from both paths via the .NET probe:
|
||||
|
||||
| Field | DCOM-marshalled (works) | Hand-rolled (fails) |
|
||||
|---|---|---|
|
||||
| Total size | 338 bytes | 162 bytes |
|
||||
| `std_flags` | `0x0A80` (SORF_OXRES4+OXRES6+OXRES8) | `0x280` (SORF_OXRES4+OXRES6) |
|
||||
| `std_public_refs` | 5 | 5 |
|
||||
| `std_oxid` / `std_oid` / `std_ipid` | random per session | random per session |
|
||||
| ncacn_ip_tcp bindings | 4 (DESKTOP-6JL3KKO, 10.100.0.48, 2x IPv6 link-local) — **no ports** | 1 (DESKTOP-6JL3KKO[<port>]) — **with port** |
|
||||
| Security bindings | 7 | 7 |
|
||||
|
||||
Tried setting `std_flags = 0x0A80` on the hand-rolled OBJREF (matching the DCOM-marshalled flag bits): **RegisterEngine2 still fails with the same 1722.** Reverted.
|
||||
|
||||
**Updated diagnosis.** The likely cause is that NmxSvc, on receiving RegisterEngine2 with a callback OBJREF, does its own SCM-side OXID resolution: it calls `IObjectExporter::ResolveOxid` against the local SCM at `127.0.0.1:135` to get the bindings for the OBJREF's OXID, then dials those bindings. Our hand-rolled OXID is **never registered with the local SCM**, so the resolution fails and NmxSvc returns `RPC_S_SERVER_UNAVAILABLE` (1722) — matching the symptom and the sub-second timing (no TCP-dial-back attempt to our listener happens at all).
|
||||
|
||||
DCOM marshalling fixes this because `CoMarshalInterface` internally registers the OXID with RPCSS, so NmxSvc's SCM-side ResolveOxid succeeds. The bindings carry no port because RPCSS-side resolution returns the dynamic port from the Windows DCOM stub layer.
|
||||
|
||||
This makes Path A the architecturally correct fix: the callback exporter must be a DCOM-managed object (registered with RPCSS) for NmxSvc to accept the callback. The hand-rolled-listener-with-explicit-port-in-OBJREF approach used by both the Rust port and the .NET reference's `ManagedCallbackExporter` doesn't satisfy NmxSvc's callback validation.
|
||||
|
||||
**Three resolution paths (each substantial):**
|
||||
|
||||
|
||||
@@ -210,9 +210,13 @@ impl CallbackExporter {
|
||||
/// Build a callback OBJREF to publish back to the AVEVA service.
|
||||
///
|
||||
/// Mirrors `ManagedCallbackExporter.CreateCallbackObjRef`
|
||||
/// (`cs:44-54`): the IID is `INmxSvcCallback`, `std_flags = 0x280`,
|
||||
/// (`cs:44-54`): the IID is `INmxSvcCallback`,
|
||||
/// `public_refs = 5`, OXID/OID/IPID come from `self.identities`, and
|
||||
/// the single string binding is `"<hostname>[<port>]"`.
|
||||
///
|
||||
/// `std_flags = 0x280` — `SORF_OXRES4 | SORF_OXRES6` (= `0x80 |
|
||||
/// 0x200`). Mirrors the .NET reference's `ManagedCallbackExporter`
|
||||
/// (`cs:48`).
|
||||
#[must_use]
|
||||
pub fn create_callback_objref(&self, hostname: &str) -> Vec<u8> {
|
||||
let binding = format!("{hostname}[{port}]", port = self.local_addr.port());
|
||||
|
||||
Reference in New Issue
Block a user