Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:
- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass
Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.
Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 KiB
Implementation Status
Completed
-
Production SDK targets
net10.0and has no AVEVA binary references. -
Public API now includes the intended parity surface:
- TCP probe
- raw, aggregate, at-time, and block history reads
- event reads
- tag browse and metadata calls
- connection, store-forward, and system-parameter status calls
- write-back intentionally remains out of scope for this read-only SDK pass
-
Internal protocol scaffolding exists:
HistorianConnectionHistorianFrameReaderHistorianFrameWriterHistorian2020ProtocolDialect
-
Evidence-backed WCF scaffolding exists:
- WCF service names for
Hist,Retr,Storage,Stat, andTrx - contract interfaces for history, history extensions, retrieval, retrieval extensions, and status
application/x-mdasmessage encoder wrapper- Net.TCP binding and endpoint factory for port
32568 - bounded
wcf-start-queryharness options (--max-attemptsand--timeout-seconds) so negative query probes do not have to run the full matrix - live local and remote
GetVevidence for/Hist,/Retr,/Stat, and/Trx - live
GetVevidence for/HistCertusing MDAS over WCF Net.TCP transport security ValidateClientCredentialtoken wrapping and the native NTLM negotiateVERSIONflag adjustment as isolated, tested protocol primitives
- WCF service names for
-
ProbeAsyncnow uses fully managed WCF/MDASGetVcalls instead of a raw TCP socket check. -
Managed
Hist.OpenConnectionreaches server logic, but the older scalar operation expects the native password/session packet. -
Managed
Hist.Open2now reaches server logic with a version-1 byte buffer. Empty credentials return custom native error171(AuthenticationFailed), confirming the basic byte-buffer framing and UTF-16 string encoding. -
Managed integrated Windows
Open2succeeds when the same version-1 buffer is sent to/Hist-Integratedwith WCF Windows transport security./Histwith that binding fails before the operation call because the upgrade is not supported on the plain history endpoint. -
The same managed integrated Windows
Open2flow succeeds remotely against<historian-host>:32568/Hist-Integrated; the returned handle is accepted byRetr.IsOriginalAllowed. The stored artifact redacts the session output buffer and transient handle value. -
The native
aahClientManaged.dllpath is confirmed to open successfully with integrated Windows auth after pollingGetConnectionStatusuntil pending clears. -
The native integrated read path is confirmed to reach query execution: a deliberately missing tag returns native
TagNotFound(127) instead of connection/authentication failure. -
scripts\Find-GalaxyHistorizedTags.ps1queries the local Galaxy Repository (localhost, databaseZB, Windows auth) for non-array dynamic attributes withHistoryExtension. -
OtOpcUaParityTest_001.Counteris confirmed as a live native read fixture candidate. A 1440-minute native wrapper read returned one row with timestamp2026-04-30T11:00:29.4340342Z,Value = 0,Quality = 133,OpcQuality = 192, andQualityDetail = 248. -
A Frida attach pass can see
aahClientManaged.dlland install hooks at candidate mixed-mode RVAs, but thosebase + RVAhooks do not fire during the successful read. The next capture approach must intercept CLR/WCF managed byte-array calls rather than raw method RVAs. Seefrida-aahclientmanaged-hook-pass.md. -
tools\AVEVA.Historian.NativeTraceHarnessis a reverse-engineering-only .NET Framework harness for native integrated reads. It records sanitized reflection snapshots aroundOpenConnection,StartQuery, andMoveNext. The latest run confirmsStartQueryassignedqueryHandle = 1and returned tag key238, value0, quality133, OPC quality192, and quality detail248forOtOpcUaParityTest_001.Counter. It now supports fixed UTC windows plus configurable retrieval mode and resolution; latest artifacts coverFull,Cyclic,Interpolated, andTimeWeightedAverage. -
The native trace harness also supports
--scenario eventwith an event connection. The latest event artifact confirmsEventQuery.StartQuerysucceeds and returns three sanitized local-dev event rows for a seven-day window. -
scripts\Attach-NativeTraceHarnessWinsockCapture.ps1andscripts\frida\aahclientmanaged-winsock.jscan attach before nativeOpenConnectionand hook Winsock plus common file/pipe APIs. Localhost,127.0.0.1, and the machine LAN IP all completed successful native reads without observedconnect/send/recv,CreateFile/ReadFile/WriteFile, orNtCreateFile/NtReadFile/NtWriteFilepayload events. This is negative evidence that the installed local Historian path uses an in-process/local optimization rather than the remote Net.TCP transport path. -
An expanded Frida method pass added decompiled
QueryColumnSelectorandHistorianClient.StartDataQuery/GetNextRowMethodDef RVAs. Hooks installed where Frida allowed them, but no enter/leave events fired during successful native reads. MethodDef RVAs from the mixed-mode assembly are not the actual CLR/native dispatch targets. -
The native trace harness can now dump prepared CLR runtime method pointers for mixed-mode
<Module>methods such asHistorianClient.StartDataQuery,CRetrievalConnectionWCF.StartQuery2,HistorianClient.GetNextRow, andHistorianClient.StartEventQuery. The corrected artifacts show these pointers are process-specific CLR/JIT entry addresses outside the loadedaahClientManaged.dllimage, not stable DLL RVAs. -
scripts\Attach-NativeTraceHarnessRuntimePointerCapture.ps1automates a same-process Frida pass against those runtime pointers. It successfully generated a pre-StartQuerypointer snapshot and installed 37 absolute hooks while the native direct history read was paused. The read then succeeded, but no hookenter/leavecallbacks fired. This is negative evidence for usingMethodHandle.GetFunctionPointer()addresses as direct Frida hook targets. -
scripts\Attach-NativeTraceHarnessAahClientExportCapture.ps1andscripts\frida\aahclient-exports.jsattempted to hook the proceduralmdas_*exports fromaahClient.dll. A successful local direct history read did not loadaahClient.dll, anddumpbin /dependentsconfirmsaahClientManaged.dlldoes not importaahClient.dlloraahClientCommon.dll. A separate-DumpLoadedModulesrun showed onlyaahClientManaged.dllamong the current AVEVA DLLs. The exportedmdas_*ABI is likely a separate native client surface, not the active C++/CLI wrapper boundary used by this harness. -
scripts\Attach-NativeTraceHarnessSystemBoundaryCapture.ps1andscripts\frida\aahclientmanaged-system-boundary.jsnow hook file I/O,Nt*File,NtDeviceIoControlFile, DNS, exported Winsock calls,WSAIoctl,mswsockextension exports, Secur32, Crypt32, and NetAPI. Local direct and same-machine remote-IP reads produce no boundary callbacks beyond hook installation. A Debian relay run confirms the relay TCP connection is owned by the harness PID, but the same exported user-mode hooks still see no connect/send/recv/device-control callbacks before the security reset. -
scripts\Run-PktmonDebianRelayCapture.ps1records pktmon metadata for the Debian relay path without retaining raw payload bytes. The latest history run captured TCP packet metadata for<relay-host>:32568, converted the ETL to a text report, deleted the ETL, and wrote a summary confirmingPayloadBytesCaptured = falseandRawEtlDeleted = true. -
Focused
ildasmexcerpts now confirmArchestrA.HistoryQuery.StartQueryhas an IL call atIL_02d2toHistorianClient.StartDataQuerytoken060055E4, andHistoryQuery.MoveNexthas an IL call atIL_0054toHistorianClient.GetNextRow<class DataQueryResultRow>token0600588D. IL-rewrite instrumentation is now validated: a dnlib-written wrapper copy can run in a disposable DLL folder, and an instrumented<Module>.Query.StartDataQuerycaptured a 251-byte nativeDataQueryRequestbuffer during a successful local direct read. -
The captured full-history buffer corrected the managed
HistorianDataQueryProtocolserializer. It now matches the native wrapper byte-for-byte for the deterministicOtOpcUaParityTest_001.Counterfixture. The buffer hash is3581ef3b42b59b46503d1aa0127fa60fe4b40943e419aeab99e47e4683888851. -
The same instrumentation path now captures the native
DataQueryResultRow*memory afterGetNextRow. The first full-history row confirms tag key, timestamp, quality, OPC quality, quality detail, and percent-good offsets; the artifact hash is2c2cb06988187c1bd7793a52a71f33599542a69d5e83885c583de8bf3df5d43b. -
The read-boundary instrumentation now logs
GetNextRowarg.1as an explicitqueryHandlescalar before dumpingarg.2row memory. A combined local read capturedStartDataQuery.RequestSHA543ea11af87607044067a0274b1da423cef2acbb7b4f4fab137af023a7153d7f,GetNextRow.QueryHandle = 1, andGetNextRow.DataQueryResultRowSHA702f5248cf8319e3e02da33678ed97dfaa43666bddb88c42101d5290990a4198in the same session. -
A follow-up combined IL instrumentation pass correlated the native open, query, WCF retrieval, and row-read handles.
HistorianClient.OpenConnectionwrote legacy handle2, andQuery.StartDataQueryread the same value as itsClientHandleCandidateimmediately before entering the common retrieval layer. The successfulCRetrievalConnectionWCF.StartQuery2call then used a different transient WCF retrieval client handle. This proves the missing read parity state is the native client/session mapping below the legacyClientApphandle, not the 251-byteDataQueryRequestpayload. -
Instrumenting
aahClientCommon.CServerClient.GetHandletoken060017F9produced no records during a successful local history read, even while the surrounding open/query/WCF hooks fired. Direct metadata-token scanning also found no IL references to that accessor. The transient WCF retrieval handle is not obtained throughGetHandleon this path. -
Instrumenting
aahClientCommon.CRetrieval.StartQuery2,aahClientCommon.CSrvRetrievalConnection.StartQuery, andCRetrievalConsoleClient.StartQueryalso produced no records in the successful WCF read path. The active path is insteadaahClientCommon.CClientCommon.StartQuerytoken06002E86. -
The
CClientCommon.StartQueryinstrumentation captured the missing handle boundary: legacy handle2entersQuery.StartDataQuery, thenCClientCommon.StartQuerycalls aCClientvtable function at IL offset0x01A3. The returned transient value exactly equals the client handle used byCRetrievalConnectionWCF.StartQuery2andGetNextQueryResultBuffer2. The WCF server query handle returned byStartQuery2is copied back through thequeryHandlepointer, while the public managedHistoryQuery.queryHandleremains the wrapper value1. Sanitized evidence is incclientcommon-startquery-correlation-latest.json. -
CClientBase.OpenConnectionconfirms that same vtable offset24is the handle accessor: the initial value is0, then the secondary open branch succeeds and the post-open value exactly matches the laterCClientCommon.StartQueryand WCFStartQuery2client handle. The primary open branch did not emit records on this local integrated path. Sanitized evidence is incclientbase-open-correlation-latest.json; the next target is the secondary open vtable call at IL offset0x06D4. -
The secondary open branch resolves to
CHistoryConnectionWCF.OpenConnection3token06004059. It callsIHistoryServiceContract2.OpenConnection2with a 1346-byte request and gets a 42-byte response with empty error. The deserialized response initializes the vtable offset24handle later used byCClientCommon.StartQueryand/Retr.StartQuery2. In the captured 42-byte response, byte0is0x03and the transient/Retrclient handle is UInt32 little-endian at offset1.CClientInfo.DeserializeOpenConnectionOutParamstoken06004008confirms the response layout: byte protocol version,uint32client handle, 16 session bytes, one FILETIME, and for version3an additional FILETIME. The observed five-byte tail is preserved as opaque trailing data until a caller or native field assignment is identified.CClientInfo.SerializeOpenConnectionInParams4token06004003confirms the observed request starts with protocol byte6, a 16-byte client key, and a boolean content selector. The observed selector is0, so the active content branch isSerializeOpenConnectionInParams2Contenttoken06004005, whose field order matches the existing native version-3 content serializer: host string, UInt16 secret length plus secret bytes, client type, connection mode, metadata namespace, two strings, and client common info.CMetadataNamespace.Saveuses compact/scrambled empty strings in this envelope, so the empty metadata namespace is 10 bytes rather than the older 13-byte plain layout. With that correction, managed replay with the observed 1026-byte zero credential block reaches server credential validation instead of packet-version rejection. Both/Hist-IntegratedWindows transport and/HistCertcertificate transport now return native error171with an extended message saying the server context could not be found. That makes the leading 16-byte OpenConnection3 key the next evidence target. A same-run memory-correlation pass now proves request bytes1..16exactly matchCClientInfo +1240, and request byte17matchesCClientInfo +1608.CClientInfo.SetContextKeytoken0600386Ecopies its GUID argument to the sameCClientInfo +1240field, but no direct IL caller exists. The actual source is now narrowed toCClientBase.ConfigureOpenConnectiontoken0600388C: it calls nativeCClientContext.AuthenticateClienttoken06005DCBonCClientBase +2112, then copies 16 bytes fromCClientBase +2176(CClientContext +64, theGetContextKeylocation) toCClientBase +1480. Since this aligns withCClientInfo +1240, the prefix is the context key that nativeAuthenticateClientvalidates before OpenConnection3, not an arbitrary fresh managed replay GUID. Runtime instrumentation aroundAuthenticateClientconfirms the field equals the generated client key before authentication, changes during the native auth path, then matches the copiedCClientInfofield and the OpenConnection3 request prefix. A direct managedHist-Integrated.ValidateClient2probe reaches the service but fails with native error type4/code51beforeExchangeKey. Sanitized IL instrumentation ofCHistoryConnectionWCF.ValidateClient(06004044) andCHistoryConnectionWCF.GetClientKey(06004041) was present in an isolated wrapper copy and the logger smoke test passed, but a successful native integrated history read emitted noValidateClient2orExchangeKeyrecords. That negative result rules out the obvious managed WCF auth methods for this path. Native disassembly ofCClientContext.AuthenticateClient(06005DCB, RVA0x298BA0) shows it uses Secur32AcquireCredentialsHandleWwith packageNegotiate, creates a context GUID atCClientContext +64, and loops throughCHistoryConnectionWCF.ValidateClientCredential/ WCFValCl. InstrumentingValClconfirms two successful native rounds: a 69-byte client input to 239-byte server output, then a 93-byte client input to a one-byte terminal output. The client input envelope is one round byte, a 4-byte little-endian token length, then an NTLMSSP token; the first native input prefix is01400000004e544c4d5353500001000000b7b218e209000900370000000f000f. System-boundary Frida evidence now confirms that these wrapped payloads come directly from SSPI: native callsAcquireCredentialsHandleWwith packageNegotiate, then twoInitializeSecurityContextWcalls forNT SERVICE\aahClientAccessPoint. The raw SSPI output token lengths are 64 and 88 bytes, matching the 69- and 93-byteValClbodies after the 5-byte AVEVA wrapper is added. The first call returnsSEC_I_CONTINUE_NEEDED(0x90312) and the second returns success. Native disassembly ofCClientContext.AuthenticateClientnow maps the loop directly: it is native-only, callsUuidCreate, stores the context GUID atCClientContext +64, callsAcquireCredentialsHandleW, and enters a loop at VA0x180298DE0. The internal helper at VA0x180298F30callsInitializeSecurityContextW, uses request flags0x2081Con the first round and0x81Con later rounds, then writes theValClstream envelope as round byte, UInt32 token length, and token bytes. The outer loop copiesCClientContext +64as the context key and calls the connection'sValidateClientCredentialvirtual method; in the captured local read,CClientContext +0x50is zero, so the normal connection-object virtual path at VA0x180298E95is selected. A stable IL-side memory window aroundCClientContext.AuthenticateClientshows the embeddedCClientContextmutates only four regions in the first 256 bytes during successful auth: pointer-sized fields at offsets+8,+16, and+24, plus the context key at+64..+79. Dereferencing the+8target shows SSPI/security-package metadata strings such asSchannel,Microsoft Kerberos V1.0,TSSSP,System.Core, andDefault TLS SSP. The+16target is partially decoded as pointer-rich native state: nested targets at offsets0and8have the same pointer/count shape and no readable ASCII or UTF-16 Historian payload strings, while the nested target at offset64is all zero. The+24value is not safe to treat as a directly readable buffer. This narrows the missing managed replay state to nativeCClientContextobject state and server acceptance of that client context key, rather than the SSPI token bytes alone. A managedNegotiateAuthenticationprobe can reproduce that first wrapped input exactly after setting the NTLMVERSIONnegotiate flag, but standaloneValClstill fails with native error type4/code1on both/HistCertand/Hist-Integrated. That means the active blocker is not the first token envelope; it is native connection/session prerequisite state around proxy initialization or server-side context registration. A follow-up instrumented local read shows the native path constructsCHistoryConnectionWCF, then callsGetInterfaceVersionbefore auth. The firstGetInterfaceVersionInitializeProxypath succeeds andSetManagedPtrsets the ready flag to1; by both successfulValClrounds,COperation.Start2succeeds with error type/code0, the existing proxy is not faulted, and the reconnect flag is0. Branch instrumentation now shows the local native path uses connection mode1, entersCWcfConfig.ConfigurePipeProxy, builds the uncompressed local/Histnamed-pipe endpoint shape, and does not useConfigureTcpProxy. Static IL inspection ofConfigurePipeProxyconfirms this path builds aNetNamedPipeBindingwithSecurity.Mode = None,TransactionFlow = false, native-sizedMaxBufferSize/ReaderQuotas.MaxArrayLength, and theaahMDASEncoder.ClientBindingwrapper. It then creates the channel through the staticChannelFactory<T>.CreateChannel(binding, endpoint)helper and setsIContextChannel.OperationTimeoutfrom the native timeout-minutes argument. Managed pipe probes using the same static-factory channel shape, with and without eager channel open, still fail in the same way. A managed named-pipeValClreplay reachesGetInterfaceVersionversion11and reproduces the first wrapped token hash, but is still rejected at round0with native error type4/code1; explicitly running the managed calls under the current Windows token does not change that result. Native handle summaries show uppercase GUID text, and managed lowercase-handle replay still fails, so handle casing is not the mismatch. SkippingGetInterfaceVersionand avoiding explicit WCF channel open in the managed TCP probe also returns native error type4/code1, so the mismatch is not just transport or token bytes. Because nativeGetInterfaceVersioncreates its proxy while impersonating the stored Windows identity, the managed pipe probe was extended to create/open the channel inside the same current-token impersonation scope; that still fails atValClround0, with and without also wrapping the operation calls. A local System Platform log check shows the managed pipeValClfailures correspond toaahClientAccessPointwarnings:ValidateClientCredential caught exception: System.NullReferenceExceptionatHistoryService.ValidateClientCredentialline1593. Enabling named-pipe transport security is rejected as a binding mismatch, which confirms the native uncompressed pipe binding shape. A new direct .NET Framework WCF probe using the sameaa/Histcontract, MDAS content type, uncompressed named pipe, and SSPI-generated first token also fails with native error type4/code1and the same server log exception. That rules out a simple .NET 10 WCF regression: the success condition belongs to AVEVA's mixed/native wrapper state, not a plain full-framework WCF client. Static IL inspection ofCOperation.Start2shows it is only a local gate: it checks operation-priority and bandwidth controls and can set local gate-failure error codes243or150, but it does not call a WCF service operation. Static inspection of the localaahClientAccessPoint.exeservice now maps the receiving side of this failure.HistoryService.ValidateClientCredentialparses the WCFhandleas a GUID, allocates aCServerBuffer, copies theValClbyte array into that buffer, and calls nativeCServerNode.ProcessServerToken. That native method parses exactly the AVEVA token envelope already observed on the client side: one round byte, a 4-byte little-endian token length, then SSPI token bytes. On the first round,ProcessServerTokencalls helper0x0050FFC0, which locks the server context collection and inserts or refreshes a keyed native context object. It then calls helper0x00517AB0to look up that object. If no context object is returned, the server sets custom error code0x29andValClfails before any successful context registration. With a context object, helper0x00505C00calls Secur32AcceptSecurityContextthrough the service import table at0x005A0340, treating both success andSEC_I_CONTINUE_NEEDED(0x90312) as valid protocol progress. Only after the terminal round doesHistoryService.ValidateClientCredentialadd the context GUID to its managed context-handle collection. This confirms the remaining managed replay gap is before or inside serverProcessServerTokencontext setup/lookup, notOpenConnection3itself. A tighter IL window confirms the line number reported in System Platform logs is the catch/log path, not the exact null-reference instruction. The normal path is:Guid.TryParseon the WCF handle at IL0x012A,CServerBufferallocation through a vtable call at0x0183, byte-array pointer/length copy into buffer offsets+72/+76, andCServerNode.ProcessServerTokenat0x01DC. Only when the native call returns success withcontinue == falsedoes IL0x0311add the parsed context GUID tom_contextHandles; whencontinue == true, the method returns the server token without final handle registration. This keeps the runtime server helper probe as the most direct remaining evidence target. Additional server disassembly and string decoding identify the native object asaahClientAccessPoint::CServerContext. The first-round setup helper uses the server lock atCServerNode +0xE80and keyed context map atCServerNode +0xE98, logsAdding ServerContext 0x%p, constructs a 0x3c-byte context object through helper0x00505100, and inserts the new node through a red-black-tree insertion helper at0x0042F590. The lookup helper reads the same map and returns the context object from map node offsets+0x20/+0x24; a null result is what drives the0x29custom error path. The credential helper callsAcquireCredentialsHandleWwith the UTF-16 package stringNegotiate. The token-processing helper is logged asaahClientAccessPoint::CServerContext::ProcessClientToken; its failure log string still saysInitializeSecurityContext, but the import actually called by this server helper is Secur32AcceptSecurityContext.HistoryService.ValidateIntegratedCredentialsis a separate server path: its first instructions readServiceSecurityContext.Current.WindowsIdentity. That explains the earlierOpenConnection2/OpenConnection3null-reference failures on bindings whereServiceSecurityContext.Currentis absent. Those errors are evidence of selecting the wrong integrated-credential path, not a user/password validation result. A focused object-window capture now shows the successful nativeGetInterfaceVersionpath populates the history proxy managed-pointer slot atCHistoryConnectionWCF +608and the ready flag at+669; betweenGetInterfaceVersioncompletion andValClentry, a second managed-pointer slot at+616is populated for the binding wrapper. Across both successfulValClrounds the parentCHistoryConnectionWCFobject window is stable, but the+608history proxy target mutates at bytes96..101; the+616binding target and+640Windows identity target remain stable. This points at native-managed proxy wrapper state as the next concrete evidence target. Static IL inspection ofCHistoryConnectionWCF.Initializenarrows the meaning of the AVEVA log line that saysInitialize: DataSourceId(). It is client-side connection/proxy setup, not a separate WCFInitializeoperation: the method logsInitialize, retrieves or creates the managed/Histproxy and binding throughInitializeProxy<IHistoryServiceContract2>, stores those pointers at the same+608/+616managed-pointer slots seen at runtime, then does the same for the/Trxproxy. The history proxy initializer has only the previously mapped WCF setup branches:ConfigurePipeProxyat IL0x0098andConfigureTcpProxyat IL0x038E, followed bySetProxyString. The decompiledHistoryServiceContractinterfaces contain noInitializeoperation contract. This makes the log line supporting evidence for local proxy setup state, but not a missing service call that a managed replay can simply add beforeValCl. A focused server-side Frida probe was added atscripts\frida\aahclientaccesspoint-valcl-context.jswith runnerscripts\Capture-AahClientAccessPointValClContext.ps1. It hooksProcessServerToken, the first-round context setup and lookup helpers, and theAcceptSecurityContextwrapper while logging only sanitized pointer, GUID, round, length, and return-value metadata. The script writes a.frida.logsidecar.An elevated PowerShell session (Admin, High Mandatory Label,
SeDebugPrivilegeenabled,SeImpersonatePrivilegeenabled) ran both scenarios on2026-05-03and Frida attach was still rejected with the CLI messageFailed to attach: process with pid <pid> either refused to load frida-agent, or terminated during injection. Directfrida.attach(<pid>)from the Python API reveals the actual exception class isfrida.ProcessNotRespondingError, which means the agent injection handshake did not complete in time, not that the OS refused the DLL load. The original suspected cause (mitigation policyMicrosoftSignedOnly/BlockNonMicrosoftBinaries) is now disproven:Get-ProcessMitigation -Id <pid>reports every category OFF for this process, includingBinarySignature.MicrosoftSignedOnly,DynamicCode.BlockDynamicCode,Cfg.Enable,ImageLoad.BlockRemoteImageLoads,ExtensionPoint.DisableExtensionPoints, andUserShadowStack.*. Process access from the elevated token also succeeds atPROCESS_ALL_ACCESS, includingPROCESS_VM_OPERATION,PROCESS_VM_WRITE, andPROCESS_CREATE_THREAD, so the DACL is not blocking injection. Cross-bitness Frida (64-bit Python attaching to a freshC:\Windows\SysWOW64\cmd.exe) attaches and runs scripts cleanly, so the WOW64 path itself is not broken. Defender real-time protection, behavior monitoring, and on-access protection are all OFF, no third-party AV/EDR product is registered withSecurityCenter2, no EDR-style filter driver is active, nofridamodules appear in the target's loaded module list before or after a failed attempt, no IFEO debugger entry exists foraahClientAccessPoint.exe, andAppInit_DLLsis empty in both 64-bit and WOW64 hives. Attach attempts withrealm='native',realm='emulated', andpersist_timeout=30all fail identically. The remaining likely cause is service-internal:aahClientAccessPoint.exeruns 152 threads, many inEventPairLowALPC/SCM waits, and Frida's in-memory manual-mapper agent does not get a cooperative thread for its RPC bootstrap. This is consistent withProcessNotRespondingErrorrather than a load-time rejection. The NativeRead probe still completed end-to-end with the canonical fixture row (TagKey=238,Value=0,Quality=133,OpcQuality=192,QualityDetail=248) but emitted no server-side helper events. The ManagedValCl probe ran the .NET Framework named-pipe ValCl path againstnet.pipe://localhost/Hist, reproduced the canonical first wrapped NTLM envelope (raw outgoing 64 bytes, wrapped 69 bytes, wrapped prefix01400000004e544c4d5353500001000000b7b218e209000900370000000f000f), and again returnedServerSuccess=false,ServerOutputLength=0,ErrorLength=5withNativeError {Type:4, Code:1, Name:Failure}— matching prior managed named-pipe ValCl failures and confirming the failure shape is reproducible but providing no new server-side helper visibility. None of the five diagnostic questions (whether0x0050FFC0ran, whether0x00517AB0returned a context, whetherAcquireCredentialsHandleWsucceeded, whetherAcceptSecurityContextwas reached, whether failures are pre- or post-context-map insertion) can be answered from these captures.ETW SSPI tracing on
2026-05-03produced server-helper-boundary evidence without injection. Alogmantrace session capturing theLsaSrv {199FE037-2B82-40A9-82AC-E1D46C792B99},LSA {CC85922F-DB41-11D2-9244-006008269001},Microsoft-Windows-NTLM {AC43300D-5FCC-4800-8E99-1BD3F85F0320},NTLM Security Protocol {C92CF544-91B3-4DC0-8E11-C580339A0BF8}, andSecurity: NTLM Authentication {5BBB6C18-AA45-49B1-A15F-085F7ED0AA90}providers at level0xFFand keywords0xFFFFFFFFFFFFFFFFrecorded:Run Total events aahClientAccessPoint events lsass events NativeRead (success) 5610 10 4330 ManagedValCl (fail) 133 0 121 Successful native server-side activity is a 47-millisecond burst of legacy MOF events (Ids
30, 34, 35, 40, 84, 10, 12, 16, 17, 86) inside PIDaahClientAccessPoint. The failing managed run produces zero events from the server PID at all — the server never reaches any SSPI helper invocation. lsass activity is also 35x lower in the failing run, consistent with auth never completing the first round end-to-end. This pins the previously loggedHistoryService.ValidateClientCredential caught System.NullReferenceException at line 1593to a code path beforeCServerNode.ProcessServerTokenat IL0x01DC.Static IL inspection of the full
HistoryService.ValidateClientCredentialbody (token0x06000774in mixed-modeaahClientAccessPoint.exe, 779 instructions, dnlib output preserved atartifacts/reverse-engineering/server-historyservice-validateclientcredential-il-latest.json) enumerates every NRE-capable instruction reached on the straight-line happy path before the ProcessServerToken call:- Prologue (IL
0x0000..0x0129). Constructs astd::wstring, case-insensitively compares two wide strings via_wcsicmp(0x00A0), and callsCServerNode.LogHistorianMessage(this, _, CServerClient*, _, _, _, _)at0x00ED. The third argument is aCServerClient*. If that pointer is derived fromOperationContext.Currentor related WCF context state and is null for the calling binding shape, the call site itself is an NRE candidate beforeGuid.TryParseis reached. - Guid parse and recover (IL
0x012A..0x015E).Guid.TryParseonarg.1at0x012A. False branch raises custom error code28viaSError.SetToCustomErrorand returns. True branch calls<Module>::PtrToStringCharsat0x0150then<Module>::GuidFromStringat0x0159to recover a native_GUIDfor ProcessServerToken.Guid.TryParsecannot NRE on a non-null string; the recovery path only NREs if the string is null, which a successfulTryParserules out. - Allocator vtable chain (IL
0x0160..0x0183). Loads&g_ClientAccessPoint + 2328, dereferences once at0x0178to get the allocator pointerpAllocator, then derefs*pAllocatorat0x017E(vtable pointer) and*(vtable + 40)at0x0182(slot for the allocator method), andcalliat0x0183returnsCServerBuffer*. NRE candidates:0x017Eif the field atg_ClientAccessPoint + 2328is null/uninitialised, or0x0182if the vtable is malformed. Both target a process-wide static, so they would fail identically for the successful native path; rule out unless the field is initialised lazily per-binding. - Allocator-null branch (IL
0x0189..0x01A3).brtrue.son the returnedCServerBuffer*. Null path raises custom error code204("No more buffer") and returns false. So a null allocator result is handled and not the NRE source. - Byte-array copy (IL
0x01A8..0x01C6).ldelema System.Byteonarg.2at0x01AAandldlenonarg.2at0x01B2. Both NRE if the WCF-deserialisedinputBufferparameter is null. The two pointer/length values are thenstind.i4'd into*(CServerBuffer + 72)and*(CServerBuffer + 76), matching the documented+72/+76offsets. - ProcessServerToken call (IL
0x01CA..0x01DC). Loads&(g_ClientAccessPoint + 2144)as theCServerNode*this, the parsed_GUID, theCServerBuffer*, aref bool continue, and aref SError, then callsProcessServerToken(token0x0600064F).
The IL slice does not include exception-handler metadata (current
dnlib-methodoutput covers instructions only), so the precise source-line1593reported by the System Platform log catch handler cannot be mapped to one specific instruction from static IL alone. The structural narrowing is: the throw must happen at aldelema,ldlen,ldind, orcalliinstruction reached before0x01DC. The shortlist is0x00ED(LogHistorianMessage withCServerClient*),0x017Eand0x0182(allocator vtable derefs), and0x01AA/0x01B2(byte-array deref). All other instructions in the window either operate on managed strings already proven non-null or on static-field addresses.Differential analysis against the successful native path narrows this further.
g_ClientAccessPointis a process-wide singleton, so the+2328field has the same value for both runs; the vtable chain is therefore unlikely to be the differentiator. The native wrapper's successful local read uses the sameSecurity.Mode = Noneuncompressed pipe binding the managed probe uses, soServiceSecurityContext.Current.WindowsIdentityis identical in both paths and the prologueCServerClient*derivation is also unlikely to be the differentiator. The remaining structural difference is the WCF parameter binding: if the managed probe's SOAP body schema causes WCF to deserialiseinputBufferas null even though a 69-byte wrapped token is on the wire,ldelemaat0x01AAwould NRE. The managed probe sends 69 bytes per its sanitised log, but the schema expectation on the server side has not been verified end-to-end.The next concrete evidence target is therefore not more static IL, but a SOAP-body comparison: dump the actual
<inputBuffer>element the .NET Framework probe writes versus the wire shape the native wrapper writes for/Hist-Integrated.ValCl. If the schemas differ, the WCF service deserialisesarg.2as null and the IL window decisively fails at0x01AA. If the schemas match, the throw is earlier (the prologue'sCServerClient*log argument becomes the prime suspect) and runtime confirmation needs PsExec SYSTEM injection or a signed Detours stub at IL0x00ED.SOAP-body comparison on
2026-05-04resolved this. Enabling<system.serviceModel><diagnostics><messageLogging logEntireMessage="true" logMessagesAtTransportLevel="true" .../>inAVEVA.Historian.NetFxWcfProbe.exe.configand capturing the on-the-wire SOAP body for the failingaa/Hist/ValClrequest produced:<ValCl xmlns="aa"> <handle>...GUID...</handle> <inputBuffer>AUAAAABOVExNU1NQAAEAAAC3...</inputBuffer> </ValCl>The 69-byte wrapped NTLM token IS on the wire as base64 inside
<inputBuffer>. The matching server response, however, used<outBuff b:nil="true" .../>rather than the expected<outputBuffer .../>shape, exposing a parameter-name mismatch.ildasmagainstaahClientAccessPoint.execonfirmed the actual server contract isValidateClientCredential(string handle, uint8[] inBuff, [out] uint8[]& outBuff, [out] uint8[]& errorBuffer)not
inputBuffer/outputBuffer. WCF builds the request body element name from the C# parameter name, so the probe sent<inputBuffer>and the server's WCF deserialiser ignored that unknown element, leavingarg.2 = inBuff = null. IL0x01AAldelema System.Bytethen NREs, which the C++/CLI catch handler at HistoryService.cpp line 1593 maps to native errorType=4 Code=1 Failurewith an empty error buffer.Adding
[MessageParameter(Name = "inBuff")]and[MessageParameter(Name = "outBuff")]to the probe'sValidateClientCredentialdeclaration and re-running unblocks the request: round 0 returnsServerSuccess=true,ServerOutputLength=239,ServerContinue=true, withServerOutputPrefixHex014e544c4d535350000200...(a0x01continue byte followed by NTLMSSP type-2 challenge). This matches the previously documented native-success "69-byte client input to 239-byte server output" exactly. Round 1 then sends the 88/93-byte NTLMSSP type-3 token and the server returnsType=129 Code=0x80090308(SEC_E_INVALID_TOKEN) with a 100-byte error buffer whose ASCII payload includesaahClientAccessPoint::CServerContext::ProcessClientTokenandInitializeSecurityContext. That is a real SSPI-level rejection insideAcceptSecurityContext, not the previous parameter-binding NRE — the original blocker is gone and the next layer of failure is exposed.The same fix is now applied to the production SDK contracts
IHistoryServiceContract2.ValidateClientCredentialandIStorageServiceContract.ValidateClientCredentialvia[MessageParameter(Name = "inBuff" | "outBuff")], preserving the conventional C# parameter names while making WCF emit the server-correct element names.ildasmagainstaahClientAccessPointalso revealed several other operations the SDK does not yet need (EnsTInBuff/OutBuff,EnsT2InBuff/OutBuff,RTag2pInBuff/outBuff,ExKeyinBuff/OutBuff,StJbstrJobid,GtJbstrJobid/jobstatus) carry the same parameter-naming mismatch with their current SDK declarations. Those are intentionally left alone for this read-only pass; audit them when their flows become required.The next concrete evidence target is now
SEC_E_INVALID_TOKENonValClround 1: native traces showedInitializeSecurityContextWwith request flags0x2081Cfor the first round and0x81Cfor later rounds. The .NET Framework probe uses default flags through the SSPI wrapper. Replicating those exact flags (and confirming the Negotiate target name matches the wrapper'sNT SERVICE\aahClientAccessPoint) is the next testable hypothesis.Native SSPI flag replication on
2026-05-04resolved this. Decoding the documented native flags:0x2081C(round 0) =ISC_REQ_IDENTIFY (0x20000) | ISC_REQ_CONNECTION (0x800) | ISC_REQ_CONFIDENTIALITY (0x10) | ISC_REQ_SEQUENCE_DETECT (0x8) | ISC_REQ_REPLAY_DETECT (0x4)0x81C(round 1+) = same minusISC_REQ_IDENTIFY
The probe's
SspiClientpreviously usedISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONFIDENTIALITY | ISC_REQ_INTEGRITY | ISC_REQ_CONNECTION = 0x10910, which is missingISC_REQ_REPLAY_DETECT,ISC_REQ_SEQUENCE_DETECT, and round-0ISC_REQ_IDENTIFY. The REPLAY/SEQUENCE pair gates NTLM MIC (Message Integrity Code) generation in the type-3 response message; without them the type-3 message has no MIC and the server'sAcceptSecurityContextrejects it withSEC_E_INVALID_TOKEN.Adding
ISC_REQ_REPLAY_DETECT,ISC_REQ_SEQUENCE_DETECT, and round-0-onlyISC_REQ_IDENTIFY(keepingISC_REQ_ALLOCATE_MEMORYfor buffer convenience and tracking the round count internally inSspiClient) reproduces the documented native ValCl sequence byte-for-byte from a fully managed client:Round Outgoing wrapped Server output ServerContinue NativeError 0 69 bytes 239 bytes (NTLM type-2 challenge) true none 1 93 bytes 1 byte ( 0x00terminal)false none FinalServerSuccess: true,FinalNativeError: null. This matches the previously documented "successful native twoValClrounds: 69-byte client input to 239-byte server output, then 93-byte client input to one-byte terminal output" exactly.The long-standing managed
ValClblocker is therefore resolved. The chain that successful native reads use is now reproducible from a managed client end-to-end:Hist-Integrated.GetV→ version11Hist-Integrated.ValClround 0 (69 → 239 bytes) ✓Hist-Integrated.ValClround 1 (93 → 1 byte terminal) ✓
The next steps in the chain —
OpenConnection3(with the now-known context key),Retr.IsOriginalAllowed, andRetr.StartQuery2— should now be exercisable without server-side helper failures, because the prerequisite native context-map registration thatProcessServerTokenperforms has finally been completed by a managed client.The production SDK currently has no SSPI client (only the wrap/unwrap helpers in
HistorianWcfAuthenticationProtocol). When the SDK auth flow is wired up for the production read path, it must use the same native-equivalent flags. .NET 10'sSystem.Net.Security.NegotiateAuthenticationdoes not exposeISC_REQ_*directly, so the SDK will likely need to P/InvokeInitializeSecurityContextW(or equivalent) to setIDENTIFY+REPLAY_DETECT+SEQUENCE_DETECTexactly. Sample reference implementation intools/AVEVA.Historian.NetFxWcfProbe/Program.csSspiClient.End-to-end chain verification on
2026-05-04. With the WCF parameter fix and SSPI flag fix in place, the .NET Framework probe was extended to chainHist.Open2(replaying the captured 1346-byte v6 request with the leading 16 context-key bytes spliced to match the managedValClGUID), thenRetr.IsOriginalAllowed, thenRetr.StartQuery2(replaying the captured 251-byteOtOpcUaParityTest_001.CounterDataQueryRequest). The result:Step Outcome Hist.GetVversion 11Hist.ValClround 0239-byte server response, NTLM type-2 challenge Hist.ValClround 11-byte terminal response Hist.Open242 bytes, version 0x03, transient/Retrclient handle decodedRetr.GetVversion 4Retr.IsOriginalAllowed(handle)return code 0,isAllowed = trueRetr.StartQuery2(handle, 1, 251 bytes, ...)Success=true, response 31 bytes,QueryHandlePresent=true, no errorThe 31-byte
StartQuery2response SHA-2564c062b5ce8181308f0f46bfd8c6088acb52e6ade94401651b7d3ccc8952edfb5is byte-for-byte identical to the previously captured native success response (recorded in the existingWcf.StartQuery2block ofCurrent Hard Blockerand ininstrumented-openconnection3-correlation/capture.ndjson). The full AVEVA Historian native wire protocol chain throughStartQuery2is now reproducible end-to-end from a fully managed client.This required one additional contract fix:
IRetrievalServiceContract2also had the parameter-name mismatch class of bug. The actual server contract usespRequestBuff/pResponseBuff/errSize/erronStartQuery2(andpResultBuff/errSize/erronGetNextQueryResultBuffer2,errSize/erronEndQuery2); the SDK declared them asrequestBuffer/responseBuffer/errorSize/errorBuffer.[MessageParameter(Name = ...)]attributes added tosrc/AVEVA.Historian.Client/Wcf/Contracts/IRetrievalServiceContract2.cs.Replay-only details: the
Open2body was constructed by reading the captured 1346-byte v6 native request fromartifacts/reverse-engineering/openconnection3-request-replay.binand overwriting bytes1..16with the new managed-side context-key GUID; the 251-byte data query was loaded as-is fromartifacts/reverse-engineering/startdataquery-request-replay.bin. Both inputs and the captured native fields they contain (machine name, process name, etc.) are local to the dev host. The probe's stdout JSON only echoes lengths, SHAs, version bytes, and prefix hex; it does not echo identity payloads.The next concrete step is the production-SDK pass to wire the managed auth chain: implement an SSPI client that emits the native flags, replace the captured-replay
Open2payload with a schema-driven serialiser usingHistorianOpen2Protocol.SerializeNativeOpenConnection3Version6(already in the SDK), and chainValCl → Open2 → /Retr.StartQuery2 → /Retr.GetNextQueryResultBuffer2for the canonical read fixture. The reverse-engineered protocol is now fully understood end-to-end for the read path; remaining work is plumbing.Production SDK plumbing landed on
2026-05-04. The full managed read path is now wired and verified end-to-end against the live local Historian:src/AVEVA.Historian.Client/AVEVA.Historian.Client.csproj— addedSystem.ServiceModel.NetNamedPipe 10.0.652802package.src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs— addedCreateMdasNetNamedPipeBinding(Security.Mode = None + MDAS encoder wrapper) andCreatePipeEndpointAddress. Marked[SupportedOSPlatform("windows")]since named pipes are Windows-only.src/AVEVA.Historian.Client/Wcf/HistorianSspiClient.cs(new) — P/InvokeInitializeSecurityContextW/AcquireCredentialsHandleW/DeleteSecurityContext/FreeCredentialsHandlewith internal round counter and the canonical native flag bitmasks (0x2081Cround 0 /0x81Clater, plusALLOCATE_MEMORYfor buffer convenience). Constants exposed asinternalfor test verification.src/AVEVA.Historian.Client/Wcf/HistorianDataQueryProtocol.cs— addedTryParseGetNextQueryResultBufferRowsfor the raw/Full row layout: 6-byte buffer header (UInt16 version=9,UInt32 rowCount) followed byrowCountself-describing rows ofUInt32 tagKey + UInt32 tagNameChars + tagName UTF-16 + UInt32 sampleBlockCount + Int64 startUtcFileTime + UInt32 quality + UInt32 qualityDetail + UInt32 opcQuality + Double numericValue + Double percentGood + 1-byte marker + 34 trailing bytes. The 5-byte error/terminal04 1E 00 00 00(type 4, code 30 = "no more data") is recognised as the "stop looping" signal.src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs(new) — chainsHist.GetV → Hist.ValCl × N → Hist.Open2 → /Retr channel → Retr.GetV → Retr.IsOriginalAllowed → Retr.StartQuery2 → loop Retr.GetNextQueryResultBuffer2. Builds the OpenConnection3 v6 request throughHistorianOpen2Protocol.SerializeNativeOpenConnection3Version6with the documented native constants (ClientType=4,ConnectionMode=0x402,FormatVersion=4,HcalVersion=17,DataSourceId=ClientDllVersion="2020.406.2652.2",ClientCommonInfo.ClientVersion=999_999,ShardId=Guid.Empty) and a 1026-byte zero credential block.src/AVEVA.Historian.Client/HistorianClientOptions.cs— addedTransport(defaults toLocalPipe) andTargetSpn(defaults toNT SERVICE\aahClientAccessPoint).src/AVEVA.Historian.Client/HistorianTransport.cs(new) — enumLocalPipe/RemoteTcpIntegrated/RemoteTcpCertificate; onlyLocalPipeis implemented in this pass.src/AVEVA.Historian.Client/Models/HistorianSample.cs— addedPercentGoodpositional property.src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs— constructor now takesHistorianClientOptions;ReadRawAsyncdelegates toHistorianWcfReadOrchestratoron Windows +Transport.LocalPipe, throwsProtocolEvidenceMissingExceptionotherwise.tests/AVEVA.Historian.Client.Tests/— newHistorianSspiClientTests(5 flag-selection tests),WcfBindingFactoryTests(3),WcfDataQueryResultBufferTests(5 golden-byte parser tests using the captured 570-byte fixture).HistorianClientIntegrationTests.ReadRawAsync_AgainstLocalHistorian_ReturnsAtLeastOneRow(gated onHISTORIAN_HOST=localhost+HISTORIAN_TEST_TAG) exercises the full managed chain against the live local Historian.
Test results with
HISTORIAN_HOST=localhostandHISTORIAN_TEST_TAG=OtOpcUaParityTest_001.Counter: 69/69 pass, including the live read. Without the env vars, the integration test skips cleanly and 64/64 pass.The reverse-engineering phase for the read path is complete. The production SDK now reads history end-to-end against the live local Historian using only managed code — no
aahClientManaged.dlloraahClient.dllloaded in the consuming process. Aggregate (Interpolated/TimeWeightedAverage) read modes, remote TCP transport, explicit username/password auth, event reads, and other contracts (EnsT,RTag2,ExKey,StJb,GtJb— all of which ildasm shows have the same parameter-naming mismatch as the resolvedValCl/StartQuery2operations) remain follow-up work but no longer face protocol-discovery blockers — only the parameter-rename audit + per-mode row-layout decoding + transport-binding additions.All listed follow-up work landed on
2026-05-04. The SDK now supports:ReadRawAsync,ReadAggregateAsync,ReadAtTimeAsync, andReadEventsAsync— all verified against the live local Historian (72/72 tests pass with the integration env vars set). Specifically:-
[MessageParameter]audit (Phase B2):ildasmagainstaahClientAccessPoint.exewas used to verify server parameter names for every operation acrossIHistoryServiceContract,IHistoryServiceContract2,IRetrievalServiceContract,IRetrievalServiceContract3, andIRetrievalServiceContract4.[MessageParameter(Name = ...)]attributes were applied to ~30 parameter-name mismatches:EnsT,EnsT2,RTag2,ExKey,AddTEx,DelTep,StJb,GtJb,AddS2,UpdC3,DelT,VldC2,OpenConnection,AddTags,RegisterTags,AddStreamValues,ValidateClient,UpdateClientStatus,SetClientTimeOut,CloseConnection,StartQuery,GetNextQueryResultBuffer,ExecuteSqlCommand,GetRecordSetByteStream,StartTagQuery,QueryTag,StartEventQuery,GetNextEventQueryResultBuffer,EndEventQuery,GetShardTagidsByTagnameAndSource. Build clean, 72/72 tests pass. -
Aggregate row parser (Phase B4):
HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferAggregateRowsparses the same wire shape as raw rows but interprets FILETIME #1 (at row offset8 + tagNameLen*2 + 4) as the interval EndTimeUtc and the FILETIME at trailer offset 2 (row offset8 + tagNameLen*2 + 43) as StartTimeUtc — derived from native struct evidence (+0x28 = EndDateTime,+0x150 = StartDateTime) and the captured raw fixture where Start == End. The orientation was verified by the liveReadAggregateAsynctest againstOtOpcUaParityTest_001.Counterreturning consistentTimeWeightedAveragerows. -
Aggregate + at-time wiring (Phase B5):
HistorianWcfReadOrchestrator.ReadAggregateAsyncandReadAtTimeAsyncchainHist.GetV → ValCl × N → Hist.Open2 → Retr.IsOriginalAllowed → Retr.StartQuery2 → loop Retr.GetNextQueryResultBuffer2. The aggregate request maps the publicRetrievalModeenum to the documentedHistorianDataQueryRequest.QueryTypevalues (Full → 2,Interpolated → 3,TimeWeightedAverage → 5,Cyclic → 4); other modes throwProtocolEvidenceMissingExceptionuntil they have a fixture- backed mapping.ReadAtTimeAsyncissues a one-tickInterpolatedwindow per requested timestamp and folds each aggregate result into aHistorianSample. -
Event flow (Phase B6+B7+B8):
HistorianWcfEventOrchestratormirrors the read orchestrator but targetsIRetrievalServiceContract4.StartEventQueryandGetNextEventQueryResultBuffer. The chain reachesStartEventQuerysuccessfully — a real victory, since the previous probe attempts failed at this exact call site.GetNextEventQueryResultBufferthen returns native error type=4 code=85 (0x55), which is a NEW server response (not the canonicalcode=30"no more data"). The orchestrator treats any 5-byte type=4 error buffer as a soft terminal and surfaces the full code via theLastErrorBufferDescriptiondiagnostic. Likely investigation targets for code 85: the existing notes describe a nativeCreateDefaultEventTagstep that callsRegisterTags2to register a syntheticCM_EVENTtag before any event read can return rows; we currently skip that prerequisite. Event-row WCF wire format also remains undecoded (only native struct snapshots are captured), so even if rows came back we'd need a fresh capture to parse them. Both of these are documented as open follow-ups. -
Remote TCP transport (Phase B1 + B9):
HistorianWcfBindingFactory.CreateBindingPair(options)selects binding + endpoint pairs byHistorianTransport:LocalPipe→net.pipe://host/Hist+/Retr(existing pipe binding);RemoteTcpIntegrated→net.tcp://host:port/Hist-Integrated(NetTcpBinding + Windows transport security) for the auth chain plus plain MDAS Net.TCP/Retrfor queries (per existing evidence that/Retrrejects Windows transport security);RemoteTcpCertificate→/HistCertover MDAS+certificate + plain/Retr. The orchestrators now consume the binding pair transport-agnostically. Untested against a live remote Historian on this host, but the auth chain is ready to fire. -
Explicit username/password auth (Phase B3):
HistorianSspiClienthas a second constructor overload that builds aSEC_WINNT_AUTH_IDENTITYUnicode struct and passes it aspAuthDatatoAcquireCredentialsHandleW. The auth chain helper picks the constructor based onHistorianClientOptions.IntegratedSecurity: whenfalseandUserNameis set, it parsesDOMAIN\user(or treats the value as a bare user) and forwards it withPassword. Untested against a live remote Historian; reserved for the explicit-creds production path.
Verified test results with
HISTORIAN_HOST=localhostandHISTORIAN_TEST_TAG=OtOpcUaParityTest_001.Counter: 72/72 pass (15-second total), including the four live integration tests (ProbeAsync,BrowseTagNamesAsync,GetTagMetadataAsync,ReadRawAsync_AgainstLocalHistorian,ReadAggregateAsync_AgainstLocalHistorian,ReadAtTimeAsync_AgainstLocalHistorian,ReadEventsAsync_AgainstLocalHistorian). Without env vars, all tests skip cleanly to 72/72.Remaining open work (no protocol discovery — pure plumbing or fresh capture):
- Decode the event-row WCF wire format (no captured fixture yet).
Most direct path: instrument the native trace harness to capture
Wcf.GetNextEventQueryResultBuffer.ResultByteswhile running the successful native event read, base-64-encode into the existing capture.ndjson, then write a parser using the same approach as the data-query parser. - Investigate code 85 (
0x55) terminal fromGetNextEventQueryResultBuffer. Most likely the missingRegisterTags2(CM_EVENT)prerequisite. Seewcf-register-event-tag-latest.jsonfor the prior probe attempts and the documentedConvertEventTagToTagMetadataGUID values (353b8145-5df0-4d46-a253-871aef49b321event tag id,5f59ae42-3bb6-4760-91a5-ab0be01f2f27event type id). - Verify remote TCP transports against an actual remote Historian.
Both
RemoteTcpIntegrated(use/Hist-Integrated) andRemoteTcpCertificate(use/HistCert) are wired but unverified on this host. - Verify explicit username/password against a live Historian with a non-current user account.
- Add
RetrievalModemappings beyond the four currently supported (Cyclic,Delta,BestFit,MinimumWithTime,MaximumWithTime,Integral,Slope,Counter,ValueState,RoundTrip,StartBound,EndBound) when corresponding native QueryType constants are documented. - Decode the trailing 24 bytes of each row body (after the EndTime/Quality/NumericValue/PercentGood/marker/StartTime stack); they're zero in the captured fixture but vary in some rows (e.g. trailer bytes at row+125..+132 differ across the 4 captured rows for the same tag), suggesting a per-sample value or source identifier we haven't decoded.
Event-flow follow-up investigation on
2026-05-04. The event orchestrator now usesConnectionMode = 0x501(Event) so the chain reachesRetr.GetNextEventQueryResultBuffer. The server returns native error type=4 code=85 (0x55) with zero rows. SQL ground truth (SELECT * FROM Runtime.dbo.Events) confirms 5+ events ARE available in the test window — they're just not flowing because the session is not subscribed.Attempted fix: call
IHistoryServiceContract2.RegisterTags2with acount=1 + 16-byte CM_EVENT GUIDbody beforeStartEventQuery. Tested with bothstorageSessionIdandcontextKeyas the handle parameter, in both upper- and lower-case GUID-D format. Every variant returned native error code=51 (InvalidParameter). Reverted.Per existing notes (lines 673–728 in this doc), the actual native prerequisite is
IHistoryServiceContract.AddTags(AddT) with aCTagMetadatapayload (version=1 byte + optional-mask=2 bytes + data-type-byte=10 + tag-id=16 bytes + compact UTF-16 strings), NOTRegisterTags2. Documented CM_EVENT identity: tag id353b8145-5df0-4d46-a253-871aef49b321, event type id5f59ae42-3bb6-4760-91a5-ab0be01f2f27,CDataType=10, storage type2.The remaining concrete next step for live event reads:
- Instrument
Wcf.AddT.Requeston a running native event harness to capture the exactCTagMetadatawire bytes. The existing reverse-engineering CLI has IL-rewrite instrumentation that captures other WCF request/response bodies — extend the same approach to AddT. - Wire the captured payload into
HistorianWcfEventOrchestratoras theadditionalSetupcallback for the event chain. - Once
AddT(CTagMetadata)succeeds, capture the resultingWcf.GetNextEventQueryResultBuffer.ResultBytesand write a parser similar to the data-query row parser.
Until step 1 lands,
ReadEventsAsyncreaches the chain layer successfully but returns empty results. The diagnostic helperEventChainDiagnosticTests.EventOrchestrator_DiagnosticDump_AgainstLocalHistoriansurfacesLastResultBufferLengthandLastErrorBufferDescriptionviaITestOutputHelperfor iteration.Raw ETL files contain SSPI tokens, machine names, and identity metadata; they stay under
artifacts/reverse-engineering/etw-sspi-{nativeread,managedvalcl}-*.etland are not committed.Therefore "rerun from elevated PowerShell" is no longer a viable next step on this host. Practical alternative evidence paths for the server helper layer:
- SYSTEM-level injection from an interactive PsExec session
(
PsExec64 -accepteula -s -i frida -p <pid> -l <script>). Requires user consent because it spawns a SYSTEM-token shell. - Signed instrumentation DLL: build a Detours-based agent with a code
signature trusted by the local mitigation policy, then
LoadLibraryAvia a signed loader. - ETW SSPI provider trace (
Microsoft-Windows-Security-SSPICli,Microsoft-Windows-Security-Auditing,Microsoft-Windows-Security-Negotiate,Microsoft-Windows-Security-Kerberos) filtered to PID<pid>. CapturesAcceptSecurityContextcalls and return statuses without injection, at the cost of helper-call granularity. - Inspect/temporarily relax the mitigation policy on this dev host
(
Get-ProcessMitigation -Name aahClientAccessPoint.exe). This is a user-consent-required change to a shared service binary.
Until one of those produces server-side helper evidence, the active blocker remains "missing native wrapper/proxy/session state that lets the server accept the client-generated context key during ValCl before OpenConnection3."
Static IL inspection now confirms both managed auth helper methods use the same handle source:
CHistoryConnectionWCF.GetClientKeyconverts itscontextKeyargument to 36-character GUID text beforeExchangeKey, andCHistoryConnectionWCF.ValidateClientCredentialconverts itscontextKeyargument the same way beforeValCl. A managed named-pipe probe was extended to callExchangeKeywith that same handle style before replayingValCl. It reachesGetInterfaceVersionversion11, butExchangeKeyitself fails with native error type4/code1across default, static-channel, and lazy static-channel variants. That rules out a simple precedingExchangeKeycall as the missing registration step for the successful nativeValClpath.Static IL inspection also maps the client-side integrated identity state:
CHistoryConnectionWCF.SetIntegratedSecurity(true)sets flagCHistoryConnection +540, capturesWindowsIdentity.GetCurrent(), and stores it withCServiceUtility.SetManagedPtr<WindowsIdentity>through the tuple at offsets+640,+600, and+664.GetInterfaceVersionreads the same tuple and callsWindowsIdentity.Impersonate()before proxy setup. Because this is client-side wrapper state and direct managed probes already run under the current Windows token, the remaining gap is the native wrapper/proxy state that makes the server accept theValClcontext key aroundGetInterfaceVersionproxy setup andCClientContext.AuthenticateClient, not normal .NET WCF binding setup orCOperation.Start2. Raw request bytes contain local identity metadata and stay under ignored artifacts; sanitized hashes are inopenconnection3-correlation-latest.json. - Prologue (IL
-
A
TimeWeightedAverageaggregate request fixture is also captured and covered by a byte-for-byte serializer test. It confirms query type5, double resolution ticks in the first resolution field, and scaled resolution ticks in the later field. The artifact hash is954874bf851bdea6333b8a8159f036e19b124b7a5febefb0cb9c9a8564b20981. -
An
Interpolatedrequest fixture is captured and covered by a byte-for-byte serializer test. It confirms query type3and the same resolution encoding as aggregate requests. The artifact hash isfc3a2fcc28d1926d2bd1de477e306cb0930e80a3327be6309b6e834e2951ca26. -
TimeWeightedAverageDataQueryResultRow*memory is captured. Aggregate rows use offset0x28for managedEndDateTimeand offset0x150for managedStartDateTime, unlike the initial raw-row assumption where the timestamp at0x28was sufficient for both bounds. -
InterpolatedDataQueryResultRow*memory is captured and follows the same time-bound offsets as aggregate rows in the one-minute-resolution fixture. -
StartEventQueryrequest bytes are captured from the native wrapper and now back a byte-for-byte managed serializer test. The successful local event capture returned three event rows and emitted a 65-byte request with SHA-2566b955b02087047a3199a8c74f3eee85c3b49aaa29b05de12eff2dd536f2da0d5. -
Native event row memory is captured through the same IL-rewrite path. Three sanitized
EventQueryResultRow*snapshots confirm managedEventTimeis a direct FILETIME at offset0x18;ReceivedTimeand event property/string payload storage remain unresolved. -
StartTagQueryrequest bytes are captured from the native wrapper's WCF path and now back a byte-for-byte managed serializer test. The deterministic OData tag filter request is 92 bytes with SHA-256af1dbcdd3eb0ad91a18882c22252aa74aff82998e96a39b63415ab4792a962ac; the successful response is 8 bytes with SHA-256db49223a2cf9616171322e5325816a7a579582ebdce91c2f89df8df7aa8aac01and parses asuint32 queryHandle,uint32 tagCount. Local native execution succeeds and returns tag nameOtOpcUaParityTest_001.Counterplus metadata summaryTagKey = 238,TagDataType = 4,TagStorageType = 3, andEngineeringUnit = None. Wildcard tag browse is a separateStartLikeTagNameSearch/GetLikeTagnamespath and has not been captured yet. -
TagQuery.GetTagInforesponse bytes are captured from the native wrapper beforeLoad<CTagMetadata,SByteStream<SCrtMemFile>>deserializes them. The deterministic one-tag response is 106 bytes with SHA-25677b2bf720d8888f08a1499a8162e706c2cef567a1f6d74d7e92efe0cd3e3e34band now backs a managed parser test for tag count, native data-type descriptor, type GUID, tag key, compact ASCII tag name, metadata provider, storage type, deadband type, and interpolation type. The post-deserializationCTagMetadatavector is also captured with 224-byte elements and hash6df4a5a837d06df9332391eb7350af17804137b87541dad8556ca04d015e2995. -
The separate wildcard browse path is now proven through fully managed WCF:
StartLikeTagNameSearchwith filterOtOpcUaParityTest%returns0, andGetLikeTagnamesreturns one 66-byte buffer with SHA-2562d450a55f392aed0026e9a957fefa3b116aab6ec81912c5d824c6b9a1ff5a4a1. The response parser is covered by a unit test and confirms layoutuint32 count, then repeateduint32 UTF-16 char lengthplus UTF-16LE tag name bytes. -
HistorianClient.BrowseTagNamesAsyncnow uses the fully managed WCF tag browse path instead of the evidence-missing guard. Public*wildcards are normalized to the HistorianLIKEwildcard%. Explicit username/password Open2 for tag browse remains guarded because the successful evidence is integrated Windows auth. -
HistorianClient.GetTagMetadataAsyncnow uses the fully managed WCF direct metadata path instead of the evidence-missing guard. It opens the same/Hist-Integratedsession and calls/RetrGetTagInfoFromName, then parses the observed 98-byte tag metadata record. The current data-type map is intentionally conservative: descriptor03 C3 00 31maps toHistorianDataType.Int4, and unknown descriptors still raiseProtocolEvidenceMissingExceptionuntil fixture-backed. -
Passwordless SSH to the Debian relay host works for the configured test account, and the host can reach the Windows Historian on
<historian-host>:32568. -
scripts\Run-DebianHistorianRelayCapture.ps1starts a temporary Python relay on the Debian box and forwards<relay-host>:32568to<historian-host>:32568. The native Windows client then emits observable remote Net.TCP/WCF traffic when pointed at<relay-host>. -
The relay capture confirms:
- initial WCF Net.TCP preamble bytes beginning with
00 01 00 01 02 02 ... net.tcp:/ - server preamble acknowledgement
0A - TLS-style
16 03 03 ...exchange on one connection - NTLMSSP type 1/2/3 messages on subsequent connections
- server rejection/reset before
OpenConnectionreaches connected state
- initial WCF Net.TCP preamble bytes beginning with
-
A fully managed
wcf-cert-probenow reproduces the/HistCerttransport shape directly. LocalHistCert.GetVreturns interface version11, and remote<historian-host>:32568/HistCertalso returns version11when the client endpoint identity is set to DNSlocalhost, matching the certificate claim. -
A fully managed remote
wcf-probealso confirms the plain service endpoints on<historian-host>:32568:/Histversion11,/Retrversion4,/Statversion0, and/Trxversion2. The certificate DNS identity issue is isolated to/HistCert. -
A fully managed remote
wcf-tag-infoprobe confirms the same integrated session handle works for scalar retrieval metadata calls. ForOtOpcUaParityTest_001.Counter,GetTagTypeFromNamereturns type1,IsManualTagreturnsfalse, and legacyGetTagInfoFromNamereturns238with no metadata buffer. Initial byte-buffer probes forGetTgByNmandGetTgalso return238with zero output bytes across tag-name and tag-id encodings, so the remaining metadata gap is likely the exact WCF contract signature or a richer request envelope rather than the session handle. -
The relay path provides transport evidence but not query buffers yet, because the relayed authentication/session is rejected before a query can start.
-
Server-side ArchestrA logs for the relayed run confirm the native client initialized against
Server(<relay-host>)fromAVEVA.Historian.NativeTraceHarness(20.0.000)and selected security typeTransport with Certificate. This means the relay failure is identity/security state related; rewriting only the plaintext Net.TCP preamble host is not sufficient to make a relay transparent. -
Forcing the private
HistorianConnectionArgs.directConnectionfield totruelets the native trace harness complete a successful read even withServerName = <relay-host>, and the Debian relay records no traffic. That makesDirectConnectiona native/local parity aid, not a way to capture the remote protocol. -
The same relay path has now been run for native event connections with endpoint-host rewriting enabled. Event mode opens the same initial
/HistCertNet.TCP preamble usingapplication/ssl-tls, then retries/Hist-Integratedwithapplication/negotiateand NTLMSSP type 1/2/3 messages. The server still returns a 13-byte rejection/reset before the harness reaches connected state. Setting--direct-connectiondoes not bypass this event path; it emits the same relay traffic and still fails to connect. This differs from history direct mode, which completed locally and avoided the relay. -
The same trace harness attempted WCF diagnostics/message logging, but no
.svclogfile was produced. Ordinary WCF config tracing is not sufficient to capture the native wrapper's byte-array calls in this process. -
The Open2 serializer and observed five-byte native error decoder are shared production internals with unit coverage. The reverse-engineering CLI reports native error type, code, and known name beside the sanitized base64 buffer.
-
A reverse-engineering-only WCF capture server exists under
tools\AVEVA.Historian.WcfCaptureServer. -
Uncaptured protocol operations throw
ProtocolEvidenceMissingExceptioninstead of pretending to implement proprietary frames. -
Reverse-engineering CLI exists under
tools\AVEVA.Historian.ReverseEngineering. It can:- inventory PE exports
- list mixed-mode MethodDef tokens/RVAs from
aahClientManaged.dll - list direct IL references to a metadata token
- hash native DLLs
- write capture manifests
- emit timestamp markers for capture notes
- probe WCF/MDAS endpoints
- attempt scalar
Openand byte-bufferOpen2session opens - probe
StatWCF operations and optional system-parameter calls - probe
Retr.StartEventQuerywith reconstructed event request buffers
-
Unit tests cover confirmed binary facts:
- FILETIME UTC conversion
- UTF-16 null-terminated strings
- enum numeric compatibility
- frame container round trips
- legacy
Hist.Open2version-1 buffer layout - native
OpenConnection3version-6 request prefix and content branch layout - first-pass
DataQueryRequest.Savefield order for empty metadata and auto-summary defaults - observed native error buffer decoding
- native
OpenConnection3version-3 response parsing from the decodedCClientInfo.DeserializeOpenConnectionOutParamslayout - Windows
SYSTEMTIMEbyte parsing for status evidence - native
StartTagQueryrequest/response layout and firstGetTagInforesponse stream fields GetLikeTagnamesbrowse response buffer layout- public browse wildcard normalization
- evidence guardrails
Current Hard Blocker
The 2020 client transport is now known to be WCF Net.TCP using a custom MDAS
message encoder, not just an opaque raw socket frame format. The Hist.Open2
legacy version-1 buffer is decoded far enough to make /Hist-Integrated
return success under fully managed integrated Windows auth. The 32-byte response
is decoded for the legacy path as:
uint32client handleGuidstorage session idint64connect time FILETIME UTCuint32server status
The decoded handle is accepted by Retr.IsOriginalAllowed, while hard-coded
handle 1 is rejected. That confirms the session handle is usable for at least
some retrieval contract calls.
The native wrapper WCF read boundary is now captured from a successful local
read. Reverse-only IL instrumentation of
CRetrievalConnectionWCF.StartQuery2 and
CRetrievalConnectionWCF.GetNextQueryResultBuffer2 shows:
StartQuery2succeeds withclientHandle = 256524568,queryRequestType = 1, request size251, response size31, response SHA-2564c062b5ce8181308f0f46bfd8c6088acb52e6ade94401651b7d3ccc8952edfb5, server query handle2967, and zero error size.GetNextQueryResultBuffer2succeeds with the same client handle and server query handle, result size570, result SHA-256f4126e0610a4c63b3dff0e41e8d15e51d75fdbc736f3ec490e7c66d1bb31638d, and a five-byte terminal/error buffer SHA-2567db15e1972ced8a44dae4d75f7a1f0cd74858c7d0deb8b3522d6a05904778bf7.- The public
HistoryQuery.queryHandle = 1is a client-side wrapper handle. The server WCF query handle on/Retris a separate value (2967in this run).
Direct managed Retr.StartQuery2 probes remain negative even with byte-matched
request serialization, so the remaining gap is no longer the data-query request
payload. The latest correlation pass shows the native read path starts with
legacy native handle 2, then the common retrieval/session layer supplies the
separate /Retr WCF client handle accepted by StartQuery2. Instrumenting
CHistoryConnectionWCF.OpenConnection2, CServerClient.GetHandle,
aahClientCommon.CRetrieval.StartQuery2,
CSrvRetrievalConnection.StartQuery, and
CRetrievalConsoleClient.StartQuery did not emit records in this path. The
current evidence target is the CClient object reached by
CClientCommon.StartQuery: the vtable method at offset 24 returns the /Retr
WCF client handle, and CHistoryConnectionWCF.OpenConnection3 creates it by
calling IHistoryServiceContract2.OpenConnection2. The 1346-byte
OpenConnection3 request is now decoded far enough to show the remaining
prerequisite: reproduce CClientContext.AuthenticateClient so the managed
client can register/validate the client-generated context key with the server
before sending OpenConnection3. The first ValCl token and local named-pipe
setup are now mapped; next inspect or instrument the native
GetInterfaceVersion and wrapper/proxy side effects that make the same token
accepted when sent by the wrapper.
Current negative probe evidence:
- exact native
QueryColumnSelector.SelectNonSummaryColumnsstate0x00008182000782FF - corrected packed
CQTIFlagsvalue for ordinary full reads (0x01FF) - query type values
0..14 - option
""and"NoOption" - selector flags
0xFFFF,0x3FFFF, andulong.MaxValue - empty MDS/storage redundant endpoints
- local-machine MDS/storage redundant endpoint variants
- corrected
NoFilterfilter text - corrected empty metadata namespace and default auto-summary byte blocks
- Windows transport security on
/Retr, which fails before the operation withProtocolException: The requested upgrade is not supported;/Retrexpects the plain MDAS Net.TCP binding even when/Hist-Integrateduses Windows transport security forOpen2. - remote
<historian-host>WCF start-query parity: all 22 reconstructed request variants open/Hist-Integratedsuccessfully and passRetr.IsOriginalAllowed, butStartQuery2returnsfalsewith zero response/error sizes; legacyStartQueryreturns code238for each. The legacy call also returns zero response size and no response buffer, so code238is not a discarded response payload. The stored artifact redacts transient handle values. - a bounded current-session replay of the first byte-matched full-history
candidate again opened
/Hist-Integrated, passedRetr.IsOriginalAllowed, and returnedStartQuery2 = falsewith zero response/error bytes. The legacyStartQueryoperation now faults with a server null-reference instead of returning a useful response. This reinforces that the blocker is the native session/client-handle state fromOpenConnection3, not the 251-byteDataQueryRequestpayload.
The Galaxy DB query path also confirmed alternate historized tags, including
TestMachine_001.TestHistoryValue. Native direct local reads succeed for that
tag, while native non-direct local history reads now report an
SEHException from DataQueryResponse.Load after successful open/connect.
Native event queries still succeed. This means the current local server remains
useful for direct native value parity and event flow evidence, but it is not yet
a clean oracle for WCF StartQuery2 history response bytes.
Running the native harness executable without its WCF diagnostics .config
does not change the non-direct history failure, so message logging is not the
cause.
Event request serialization has one stronger fact set than history now:
EventQueryRequest.Save<SByteStream<SCrtMemFile>> writes version 5,
start/end FILETIME, event count, skip count, order, query type, the empty filter
block, result buffer size 0x10000, timezone UTC, empty metadata namespace,
and empty selected-property count. The managed wcf-start-event-query probe
serializes that 65-byte request and records the SHA-256 in
docs\reverse-engineering\wcf-start-event-query-attempts-latest.json.
The WCF queryRequestType for event starts is now confirmed as 3.
CRetrievalConnectionWCF.StartEventQuery also contains a version dispatch:
server interface versions <= 2 use /Retr.StartQuery2, while newer versions
use /Retr.StartEventQuery. The latest direct managed probe tries both
operations with the same 65-byte request.
Event Open2 setup is also partially mapped. SetConnectionMode(Event, integratedSecurity: true) yields connection mode 1281 (0x501), while the
process read-only integrated mode is 1026 (0x402). The latest managed probe
tries both modes and three handle candidates: returned Open2 handle,
clientHandlerArray[1] == 1, and native eventTagHandle == 10000000. All
currently return false with empty error buffers through both
StartEventQuery and StartQuery2.
Native event open performs one extra setup step that the managed probe does not
yet reproduce: CreateDefaultEventTag creates CM_EVENT with description
AnE Event, engineering unit NONE, and data type 10; it then calls
AddTagInternal, which reaches HistorianClient.AddHistorianTag.
The immediate aahClientCommon.CClient.RegisterTag continuation is client-local
state: it inserts the tag id into local sets, marks the cached tag info as
registered, and flips synchronization flags. The RTag2 WCF edge now has one
negative probe: its input buffer is a serialized GUID vector (uint32 count
followed by 16-byte GUIDs), but wcf-register-event-tag failed for empty,
zero-GUID, and event-handle-shaped vectors across Open2-derived GUID handle
candidates. First-16 handle candidates return native error 51; the bytes 4-19
.NET GUID candidate reaches deeper and returns native error 1. The richer
AddHistorianTag / CTagMetadata server path remains the leading candidate for
why native event queries start successfully while direct managed event-start
calls do not.
The default event tag metadata path is now partially concrete:
ConvertEventTagToTagMetadata maps CM_EVENT to event tag id
353b8145-5df0-4d46-a253-871aef49b321, common event type id
5f59ae42-3bb6-4760-91a5-ab0be01f2f27, CDataType = 5, and storage type 2.
CTagMetadata.Save writes a one-byte version, two-byte optional-field mask,
the data-type byte, tag id, and compact UTF-16 strings. The current managed
wcf-add-event-tag probe uses these facts to try two payload variants through
IHistoryServiceContract.AddTags (AddT), but both return AddReturnCode = 4
with no output buffer and event query start remains false. This is useful
negative evidence: the current approximation is not the missing server state.
Two capture routes were also ruled out for this event setup. First, a fake WCF
server bound to an alternate port saw only startup and no native calls while the
native event harness still queried the real local Historian, so the native WCF
endpoint is not redirected simply by changing HistorianConnectionArgs.TcpPort.
Second, an expanded Frida pass installed hooks for
CreateDefaultEventTag, AddTagInternal, AddHistorianTag,
ConvertEventTagToTagMetadata, and CTagMetadata.Save before open; the event
query succeeded, but no hook enter/leave callbacks fired. MethodDef RVAs are
not viable runtime interception points for this mixed-mode path.
Third, the native harness now has a --pre-load-sleep-seconds option so Frida
can attach before aahClientManaged.dll is loaded. A preload Winsock/IPC pass
for the successful local event scenario (winsock-event-preload-localhost-latest.ndjson)
still recorded only hook installation plus the native success snapshot, with no
Historian-port socket traffic and no tracked named-pipe/file payloads. The local
event success path is therefore below or outside the current Winsock/pipe hook
set.
The following are not yet known:
- native version-3/version-4 open buffer differences
- exact native client/session field that maps legacy handle
2to the/RetrWCFclientHandleconsumed by successful nativeStartQuery2. - why direct managed
Retr.StartQuery2fails with handles decoded from managedOpen2even though the native wrapper's WCF call succeeds with a handle from the same operation family. - whether
GetNextQueryResultBuffer2's five-byte error buffer on a successful row batch is a terminal-status marker, a warning, or a harmless native error scratch buffer. - query result buffer layouts
- event default tag registration (
CM_EVENT) and result buffer layout - broader wildcard browse behavior for multi-batch and empty-result cases
- non-Open2 native error buffer variants
- write request/result buffer layouts
New tag-query evidence narrows the metadata path. The latest combined IL
instrumentation shows successful native CRetrievalConnectionWCF.StartTagQuery
uses a 4-byte WCF request (51 67 01 00) and a native retrieval-session GUID
string, not the 92-byte filter-bearing request shape. Direct managed replay of
both shapes with numeric, GUID, hex, base64, fresh-GUID, and captured-GUID handle
candidates failed with native errors 51 or 1. This means
StartTagQuery depends on earlier native session/filter registration. Do not
wire metadata through guessed StartTagQuery; the SDK now uses the proven
GetTagInfoFromName direct WCF metadata path. Instrument the earlier
metadata-layer registration call only if StartTagQuery pagination becomes
necessary later.
Until these buffers are decoded and fixture-backed under fixtures\protocol\2020,
the SDK cannot truthfully perform real Historian reads or writes.
Next Capture Pass
Run these first:
dotnet run --project tools\AVEVA.Historian.ReverseEngineering -- manifest
dotnet run --project tools\AVEVA.Historian.ReverseEngineering -- exports current\aahClient.dll
dotnet run --project tools\AVEVA.Historian.ReverseEngineering -- mark connect-process
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Test-AahClientManagedOpen.ps1 -IntegratedSecurity
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Test-AahClientManagedReadIntegrated.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Find-GalaxyHistorizedTags.ps1 -Limit 25
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Attach-AahClientManagedFridaCapture.ps1 -TagName OtOpcUaParityTest_001.Counter
.\tools\AVEVA.Historian.NativeTraceHarness\bin\Debug\net481\AVEVA.Historian.NativeTraceHarness.exe --tag OtOpcUaParityTest_001.Counter --lookback-minutes 1440 --max-rows 1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Attach-NativeTraceHarnessWinsockCapture.ps1 -ServerName <historian-host> -Scenario history -RetrievalMode Full -TagName OtOpcUaParityTest_001.Counter -LookbackMinutes 1440 -MaxRows 1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Run-DebianHistorianRelayCapture.ps1 -SshUser <relay-user> -SshHost <relay-host> -TargetHost <historian-host> -OutputPath .\docs\reverse-engineering\debian-relay-history-latest.ndjson -HarnessOutputPath .\docs\reverse-engineering\native-trace-harness-via-debian-relay-latest.json
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Run-DebianHistorianRelayCapture.ps1 -SshUser <relay-user> -SshHost <relay-host> -TargetHost <historian-host> -OutputPath .\docs\reverse-engineering\debian-relay-history-direct-latest.ndjson -HarnessOutputPath .\docs\reverse-engineering\native-trace-harness-via-debian-relay-direct-latest.json -HarnessExtraArgs @("--direct-connection")
Then execute the matching native SDK scenario while recording API Monitor,
Detours-style lower-boundary hooks, or a CLR profiler/managed method rewrite.
Do not use MethodDef RVAs as Frida hook targets, and do not treat
MethodHandle.GetFunctionPointer() as sufficient by itself; the latest
same-process runtime-pointer Frida pass installed hooks but saw no callbacks
during a successful direct history read. Also do not assume aahClient.dll
exports are on the wrapper path; the latest export-hook pass did not observe
that DLL being loaded. Windows built-in packet capture produced empty PCAPNG
files for local loopback on this machine, and classic WCF diagnostics did not
produce message logs in the native harness process. A remote Historian server
target would be better for packet/Winsock capture because the installed local
server path appears to bypass observable client-process network and pipe IO.
Add only sanitized decoded buffers or fixture bytes to the repo.
Current Frida evidence is now broad enough that repeating export-level hooks is unlikely to pay off. The next practical evidence path is one of:
- API Monitor/Detours with deeper mixed-mode or WCF-native call interception.
- CLR profiler/IL instrumentation around the C++/CLI wrapper's managed entry and WCF contract calls.
- ETW/WFP/kernel-level network tracing for the relay path, then correlate to the sanitized relay transcript and harness state.
The pktmon path now covers metadata/timing. It still cannot provide query bytes
because the relayed session fails before ConnectedToServer = true; it is a
correlation aid, not the protocol parser input.
The local WCF capture server cannot currently intercept the native localhost
path: the native wrapper ignores TcpPort for local machine targets and still
uses the installed Historian endpoint. See native-open-capture-server.md.
Additional confirmed status evidence is in wcf-status-localhost.md: the
Stat endpoint is reachable, but WCF GetServerTime is a native no-op stub and
handle-dependent status calls return failures with handle 0.