Files
histsdk/docs/reverse-engineering/managed-wrapper-findings.md
dohertj2 c95824a65d Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
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>
2026-05-04 06:31:48 -04:00

824 lines
39 KiB
Markdown

# Managed Wrapper Findings
Source assembly: `current\aahClientManaged.dll`
Tooling used: `ilspycmd 10.0.0.8330`
## Confirmed Shape
`aahClientManaged.dll` is a mixed-mode C++/CLI wrapper. The public managed API is
under the `ArchestrA` namespace, but query execution calls native
`HistorianClient` methods through C++ pointers rather than plain P/Invoke
methods.
`dumpbin /dependents` confirms the deployed wrapper does not import
`aahClient.dll` or `aahClientCommon.dll`. It is a self-contained mixed-mode
wrapper over system DLLs and the CLR (`mscoree.dll`), so the active wrapper path
must be captured inside `aahClientManaged.dll` or at system/WCF boundaries.
The production read surface maps to these wrapper types:
- `ArchestrA.HistorianAccess`
- `ArchestrA.HistorianConnectionArgs`
- `ArchestrA.HistoryQueryArgs`
- `ArchestrA.HistoryQuery`
- `ArchestrA.HistoryQueryResult`
- `ArchestrA.EventQueryArgs`
- `ArchestrA.EventQuery`
- `ArchestrA.HistorianEvent`
## Connection Facts
`HistorianConnectionArgs` defaults:
- `TcpPort = 32568`
- `ServerName = "localhost"`
- `ReadOnly = true`
- `PacketTimeout = 1000`
- `ConnectionType = Process`
- `Compression = false`
- `IntegratedSecurity = false`
`HistorianConnectionType` is a flags enum:
- `Process = 1`
- `Event = 2`
The wrapper keeps separate client slots for process/history and event
connections.
`HistorianAccess.OpenConnection` can return `true` while the native connection
is still pending. Callers must poll `HistorianAccess.GetConnectionStatus` and
wait for `ConnectedToServer = true` and `Pending = false` before starting
queries. Starting `HistoryQuery.StartQuery` immediately after `OpenConnection`
can return native `notConnected` (`242`).
## History Query Contract
`HistoryQuery.StartQuery` eventually calls native `HistorianClient.StartDataQuery`.
It does not build or call `RetrievalServiceContract.StartQuery2` directly from
managed C#. The managed wrapper converts `HistoryQueryArgs` into native
strings, arrays, selector buffers, and enum values, then hands those to the
native `HistorianClient` implementation. That native boundary is the next
capture point for exact data-query request bytes.
Confirmed arguments include:
- Retrieval mode: cast from `ArchestrA.HistorianRetrievalMode`.
- Query format: `0`.
- Summary type: `0`.
- Tag count and UTF-16 tag pointer array.
- Start/end timestamps converted with `DateTime.ToUniversalTime().ToFileTime()`.
- Resolution, value deadband, time deadband.
- Time zone string: `UTC`.
- Version type from `DataVersion`.
- Interpolation, timestamp, quality, value selector, and aggregation enums.
- Option and filter strings converted as UTF-16 buffers.
- Selected query-column buffer.
- Max states.
- Output query handle.
Decompiled selected-column stream construction is:
- `ushort` value `1`
- 8 bytes of `QueryColumnSelector` state after
`QueryColumnSelector.SelectNonSummaryColumns`
Focused IL extraction of `DataQueryRequest.Save<SByteStream<SCrtMemFile>>`
confirmed several request-buffer details used by the .NET 10 serializer probe:
- request packet version is `ushort 3` only when option text is exactly
`NoOption`; otherwise it is `ushort 9` and the option string is serialized.
- offset `300` is serialized as a packed `CQTIFlags` `ushort` before
option/filter text, not the query-column selector flags. The low byte is
interpolation (`None` is stored as `255`; `SystemDefault` `254` is normalized
to `255`), bits `8..11` are `HistorianTimestampRule`, and bits `12..15` are
`HistorianQualityRule`. Ordinary full reads therefore write `0x01FF`
(`Interpolation=None`, `TimestampRule=End`, `QualityRule=Extended`).
- the query-column selector is serialized later as `ushort 1` followed by the
exact 8-byte selector state.
- after the MDS and storage redundant endpoint blocks, the request writes the
raw 8-byte resolution tick field again.
- empty `CMetadataNamespace.Save` writes flag byte `1` and three empty
`SaveScrambled` strings; each empty scrambled string is `ushort 1` followed
by a single zero compressed-string marker byte.
- default `AutoSummaryParameters.Save` writes `ushort 2`, two zero `int64`
values, five empty compressed strings, and the trailing zero GUID.
- `CValueSelector.Save`, `CStateCalcSelector.Save`, and `CQTIFlags.Save` each
write a single `ushort` value.
- `SRedundantEndpoint.Save` writes endpoint name and then endpoint entries as
node-name and pipe-name strings; there is no numeric port field in this block.
The resulting byte count and pointer are passed to `StartDataQuery` as the
selected query-column buffer arguments.
The decompiled call site passes the following high-level arguments to
`HistorianClient.StartDataQuery`:
- client pointer from the process connection slot
- retrieval mode as `INSQL_QUERYTYPE`
- query format `0`
- summary type `0`
- tag count and native UTF-16 tag array
- source count `0` and null source array
- start/end as UTC FILETIME values
- resolution, value deadband, and time deadband
- literal timezone `UTC`
- data version, interpolation type, timestamp rule, quality rule
- option text, with `NoOption` substituted when the option is not available
- value selector, aggregation type, selected-column byte count and buffer
- filter text and max states
- output query handle and native error structure
This explains the current managed WCF negative evidence: the reconstructed
`DataQueryRequest.Save` buffer is only one downstream native serialization
artifact, while the public wrapper path reaches that serializer through
`HistorianClient.StartDataQuery`.
`BaseQueryArgs.ProcessQueryArgs` clears `Resolution` to `0` for `Full`, `Delta`,
and `Slope` retrieval modes.
`HistoryQuery.MoveNext` calls native `HistorianClient.GetNextRow<DataQueryResultRow>`.
## Runtime Method Pointer Evidence
`tools\AVEVA.Historian.NativeTraceHarness` can now dump prepared CLR runtime
method pointers with `--dump-method-pointers <filter>`. This is reverse-
engineering-only evidence for hook targeting; it is not production SDK code.
Important artifacts:
- `runtime-method-pointers-startquery-latest.json`
- `runtime-method-pointers-startdataquery-latest.json`
- `runtime-method-pointers-getnextrow-latest.json`
- `runtime-method-pointers-starteventquery-latest.json`
The dump must include `assembly.ManifestModule.GetMethods(...)` because many
useful mixed-mode functions are global `<Module>` methods rather than ordinary
managed type methods. Confirmed runtime-discoverable methods include:
- `<Module>.Query.StartDataQuery`
- `<Module>.HistorianClient.StartDataQuery`
- `<Module>.ClientApp.StartDataQuery`
- `<Module>.CRetrievalConnectionWCF.StartQuery2`
- `<Module>.Query.GetNextRow`
- `<Module>.HistorianClient.GetNextRow<class DataQueryResultRow>`
- `<Module>.Query.StartEventQuery`
- `<Module>.HistorianClient.StartEventQuery`
The prepared function pointers are process-specific CLR/JIT entry addresses.
They are outside the loaded `aahClientManaged.dll` image in the current runs
(`FunctionPointerInModule = false`, `FunctionPointerRva = null`), so they must
not be treated as stable DLL RVAs. They can still be useful if discovered inside
the same target process immediately before attaching a hook, or if a CLR
profiler/rewrite pass resolves the runtime target directly.
A same-process Frida pass using these absolute runtime pointers is also
negative evidence so far. `Attach-NativeTraceHarnessRuntimePointerCapture.ps1`
launched the native trace harness, wrote the runtime pointer snapshot before
`HistoryQuery.StartQuery`, paused, generated a temporary Frida script from those
exact addresses, and installed 37 hooks. The native direct history read still
succeeded, but no hook `enter`/`leave` callbacks fired. This suggests
`MethodHandle.GetFunctionPointer()` is not the callable edge used by the
mixed-mode wrapper path in a way Frida can intercept directly.
## IL Instrumentation Evidence
Focused `ildasm` excerpts are stored as:
- `ildasm-historyquery-startquery-excerpt-latest.il`
- `ildasm-historyquery-movenext-excerpt-latest.il`
- `ildasm-classlist-filtered-latest.txt`
These excerpts confirm `ArchestrA.HistoryQuery.StartQuery` is IL with a normal
metadata-token call to the mixed-mode target:
- call site: `IL_02d2`
- target: `HistorianClient.StartDataQuery`
- metadata token: `060055E4`
`ArchestrA.HistoryQuery.MoveNext` has the same shape for row retrieval:
- call site: `IL_0054`
- target: `HistorianClient.GetNextRow<class DataQueryResultRow>`
- metadata token: `0600588D`
This is now a proven instrumentation path. A dnlib `NativeWrite` copy of
`aahClientManaged.dll` loads and runs in the native trace harness when placed in
a disposable copied DLL folder. A reverse-only IL rewrite of
`<Module>.Query.StartDataQuery` token `0600574B` inserted a logger immediately
after `DataQueryRequest.Save<SByteStream<SCrtMemFile>>` and
`endstream`. The instrumented wrapper completed a local direct read and emitted
the serialized `StartDataQuery.Request` buffer before the native WCF call.
The read-boundary instrumentation now also logs the explicit
`HistorianClient.GetNextRow<class DataQueryResultRow>` `arg.1` query handle
before dumping the `arg.2` row memory. A combined local read captured these
records in one session:
- `StartDataQuery.Request`: length `251`, SHA-256
`543ea11af87607044067a0274b1da423cef2acbb7b4f4fab137af023a7153d7f`
- `GetNextRow.QueryHandle`: value `1`
- `GetNextRow.DataQueryResultRow`: length `512`, SHA-256
`702f5248cf8319e3e02da33678ed97dfaa43666bddb88c42101d5290990a4198`
The harness snapshot from the same run shows `HistoryQuery.queryHandle = 1`,
so the managed wrapper field, native `GetNextRow` argument, and row buffer are
now correlated without relying on inferred state.
The next successful run instrumented the native WCF retrieval boundary itself.
`CRetrievalConnectionWCF.StartQuery2` sent the same 251-byte request through
`RetrievalServiceContract.IRetrievalServiceContract2.StartQuery2` with
`clientHandle = 256524568` and `queryRequestType = 1`. The server returned a
31-byte response buffer, SHA-256
`4c062b5ce8181308f0f46bfd8c6088acb52e6ade94401651b7d3ccc8952edfb5`, and WCF
query handle `2967`. `CRetrievalConnectionWCF.GetNextQueryResultBuffer2` then
used `clientHandle = 256524568` and `queryHandle = 2967` and returned a
570-byte result buffer, SHA-256
`f4126e0610a4c63b3dff0e41e8d15e51d75fdbc736f3ec490e7c66d1bb31638d`.
This separates the two handles: `HistoryQuery.queryHandle = 1` is the
client-side `HistorianClient` query table handle, while `/Retr` uses the server
query handle returned from `StartQuery2`.
A later correlation run added scalar logging inside
`<Module>.HistorianClient.OpenConnection` and `<Module>.Query.StartDataQuery`.
`OpenConnection` wrote legacy native handle `2`, and `Query.StartDataQuery`
read the same `*(HistorianClient + 8)` value immediately before the vtable call
into the common retrieval layer. The WCF transport method still called
`IRetrievalServiceContract2.StartQuery2` with a different transient retrieval
client handle. That means the managed read blocker is not the public
`HistoryQuery.queryHandle`, not the legacy native handle, and not the
`DataQueryRequest` bytes; it is the session mapping maintained by the native
common retrieval layer before the WCF contract call. The sanitized correlation
summary is stored in `query-handle-correlation-latest.json`.
`aahClientCommon.CServerClient.GetHandle` returns `*(this + 2528)`, but an
instrumented copy of that accessor emitted no records during the same successful
history read path. A direct metadata-token reference scan also found no IL call
sites for token `060017F9`. The handle translation is therefore either direct
field access or a virtual/calli continuation below `CRetrieval.StartQuery2`, not
this accessor.
Additional probes ruled out that assumed `CRetrieval` branch for the successful
local WCF read path: instrumented copies of
`aahClientCommon.CRetrieval.StartQuery2`,
`aahClientCommon.CSrvRetrievalConnection.StartQuery`, and
`CRetrievalConsoleClient.StartQuery` emitted no records while the WCF boundary
hooks still fired. The active handoff is `aahClientCommon.CClientCommon.StartQuery`
token `06002E86`.
`CClientCommon.StartQuery` performs the missing handle lookup immediately before
calling the WCF retrieval method. The value returned by the `CClient` vtable
call at IL offset `0x01A3` exactly matches the
`CRetrievalConnectionWCF.StartQuery2` client handle. After the WCF call returns,
the `queryHandle` pointer contains the same server query handle later passed to
`GetNextQueryResultBuffer2`. This narrows the remaining session problem to
reproducing or replacing the native `CClient` open/session state that produces
the `/Retr` client handle, not rebuilding the data-query request envelope.
`CClientBase.OpenConnection` uses the same vtable offset `24` as a handle
accessor. At the beginning of open it returns `0`; after the secondary open
branch at IL offset `0x06D4` succeeds, the accessor returns the same transient
handle consumed by `CClientCommon.StartQuery` and WCF `StartQuery2`. The primary
open branch did not fire in the local integrated read capture. The next concrete
reverse-engineering target is the secondary open vtable call and the WCF/native
contract behind it.
That secondary branch is `CHistoryConnectionWCF.OpenConnection3`, which wraps
`IHistoryServiceContract2.OpenConnection2`. The successful local read capture
logged a 1346-byte OpenConnection3 request, a 42-byte response, and an empty
error buffer. In that response, byte `0` is `0x03` and the transient `/Retr`
client handle is UInt32 little-endian at offset `1`. Deserializing the 42-byte
response initializes the vtable offset `24` handle. Raw request bytes include
local identity/process metadata, so only hashes and redacted handle relationships
are documented in
`openconnection3-correlation-latest.json`.
The first captured full-history request for deterministic tag
`OtOpcUaParityTest_001.Counter` is 251 bytes with SHA-256
`3581ef3b42b59b46503d1aa0127fa60fe4b40943e419aeab99e47e4683888851`.
The sanitized byte dump is stored in
`startdataquery-request-buffer-latest.json`; raw generated binaries and runtime
capture files stay under ignored `artifacts\reverse-engineering`.
This fixture corrected the managed serializer in three places:
- `BatchSize` is not serialized in `DataQueryRequest`.
- A reserved `uint32 0` follows `SkipRows`.
- `AutoSummaryParameters` serializes as version `1`, two zero `int64` values,
five zero flag bytes, and a final `uint32 0`.
The production-side WCF request serializer now has a byte-for-byte test against
this native buffer.
The same instrumentation captured a `TimeWeightedAverage` aggregate
`StartDataQuery.Request` for a one-minute resolution window. It is also 251
bytes; SHA-256 is
`954874bf851bdea6333b8a8159f036e19b124b7a5febefb0cb9c9a8564b20981`, and the
sanitized dump is stored in
`startdataquery-timeweightedaverage-request-buffer-latest.json`.
Confirmed aggregate differences versus full-history:
- request query type at offset `0x02` is `5`
- first resolution field at offset `0x1E` is `double 600000000.0`
- later resolution field at offset `0xD0` is `int64 6000000000000`
- `BatchSize = 3` from `HistoryQueryArgs` is still not serialized in the
`DataQueryRequest` buffer
The managed serializer now has a second byte-for-byte fixture test for this
aggregate request.
An `Interpolated` request with the same one-minute resolution also serializes
to 251 bytes. SHA-256 is
`fc3a2fcc28d1926d2bd1de477e306cb0930e80a3327be6309b6e834e2951ca26`, and the
sanitized dump is stored in
`startdataquery-interpolated-request-buffer-latest.json`.
Confirmed interpolated request details:
- request query type at offset `0x02` is `3`
- first resolution field at offset `0x1E` is `double 600000000.0`
- later resolution field at offset `0xD0` is `int64 6000000000000`
- the rest of the default filter, metadata namespace, endpoint, and
auto-summary tail match the full-history and aggregate fixtures
The managed serializer now has byte-for-byte fixture coverage for full history,
time-weighted aggregate, and interpolated data requests.
A second IL rewrite instrumented
`<Module>.HistorianClient.GetNextRow<class DataQueryResultRow>` token
`0600588D` immediately after the call to
`Query.GetNextQueryResultRow<class DataQueryResultRow>` token `060058AF`.
The successful one-row local read emitted a 512-byte snapshot of the native
`DataQueryResultRow*` output structure. The sanitized first-pass dump is stored
in `getnextrow-dataqueryresultrow-memory-latest.json`; the artifact hash is
`2c2cb06988187c1bd7793a52a71f33599542a69d5e83885c583de8bf3df5d43b`.
Confirmed basic row-memory offsets for this full-history sample:
- offset `0x00`: `uint32` tag key `238`
- offset `0x28`: FILETIME UTC start timestamp
`2026-05-01T14:26:51.1956318Z`
- offset `0x30`: `uint32` quality `133`
- offset `0x34`: `uint32` quality detail `248`
- offset `0x38`: `uint32` OPC quality `192`
- offset `0x80`: `double` percent good `100`
The managed `HistoryQueryResult` reflection snapshot from the same run maps
these fields to tag name `OtOpcUaParityTest_001.Counter`, value `0`, quality
`133`, OPC quality `192`, quality detail `248`, and percent good `100`.
For `TimeWeightedAverage`, `GetNextRow` emits a different row-memory layout for
time bounds. The sanitized summary is in
`getnextrow-timeweightedaverage-memory-latest.json`.
Confirmed aggregate row offsets from the two returned rows:
- offset `0x00`: `uint32` tag key `238`
- offset `0x28`: managed `EndDateTime`
- offset `0x150`: managed `StartDateTime`
- offset `0x30`: `uint32` quality
- offset `0x34`: `uint32` quality detail
- offset `0x38`: `uint32` OPC quality
- offset `0x80`: `double` percent good
The third logged row snapshot duplicated the second row after the managed
enumerator reached native result code `30` (`No more data`), so row-result
instrumentation should correlate captures with the managed `MoveNext` return
status before treating every logged struct as a logical row.
`Interpolated` row memory follows the same time-bound layout in the captured
fixture. The sanitized summary is in
`getnextrow-interpolated-memory-latest.json`.
- offset `0x00`: `uint32` tag key `238`
- offset `0x28`: managed `EndDateTime`
- offset `0x150`: managed `StartDateTime`
- offset `0x30`: `uint32` quality `0`
- offset `0x34`: `uint32` quality detail `248`
- offset `0x38`: `uint32` OPC quality `192`
- offset `0x80`: `double` percent good `100`
The second logged interpolated row snapshot duplicated the first after
`No more data`, matching the aggregate instrumentation behavior.
Integrated-auth native harness evidence (`tools\AVEVA.Historian.NativeTraceHarness`,
latest JSON artifact
`docs\reverse-engineering\native-trace-harness-integrated-read-latest.json`)
confirms these runtime state transitions for
`OtOpcUaParityTest_001.Counter`:
- `HistorianAccess.OpenConnection` succeeds and sets
`clientHandlerArray[0] = 1` for the process connection slot.
- `HistoryQuery` starts with `queryHandle = 0`, `retrievalMode = Cyclic`,
`dataVersion = Original`, and no result object.
- `HistoryQueryArgs` for the read uses `RetrievalMode = Full`,
`DataVersion = Latest`, `BatchSize = 1`, `QualityRule = Extended`,
`TimeStampRule = End`, `InterpolationType = None`, `Filter = NoFilter`,
`ValueSelector = Auto`, and `AggregationType = Total`.
- `HistoryQuery.StartQuery` succeeds, normalizes `dataSourceId` from `null` to
empty string, allocates a `HistoryQueryResult`, and assigns
`queryHandle = 1`.
- First `MoveNext` returns one row with tag key `238`, value `0`,
quality `133`, OPC quality `192`, quality detail `248`, and
percent good `100`.
Later local runs show a distinction between local direct history reads and the
WCF history path:
- `--direct-connection` succeeds for Galaxy-discovered historized tags such as
`TestMachine_001.TestHistoryValue`, returning one row with value `0`, quality
`133`, OPC quality `192`, and quality detail `248`.
- non-direct local history reads currently open and connect successfully but
throw `SEHException` inside native `DataQueryResponse.Load` during
`HistoryQuery.StartQuery`; the hardened harness now reports this as
`StartQueryException` instead of crashing.
- event queries still succeed over the local non-direct path, so the Historian
service is reachable and the failure is specific to data query response
handling on the local WCF history path.
The harness accepts `--retrieval-mode`, `--start-utc`, `--end-utc`, and
`--resolution-ticks` so scenarios can be repeated with fixed UTC windows. Latest
native wrapper evidence artifacts:
- `native-trace-harness-full-latest.json`: `Full`, one raw row.
- `native-trace-harness-cyclic-latest.json`: `Cyclic`, 60-second resolution,
two rows.
- `native-trace-harness-interpolated-latest.json`: `Interpolated`, 60-second
resolution, one row.
- `native-trace-harness-timeweightedaverage-latest.json`:
`TimeWeightedAverage`, 60-second resolution, two rows.
`HistoryQueryResult` allocates a native `DataQueryResultRow` of 544 bytes.
Confirmed offsets used by the managed wrapper:
- `0`: tag key (`uint`)
- `4`: data type discriminator (`int`)
- `8`: tag name (`std::wstring`)
- `40`: end timestamp (`FILETIME`)
- `48`: quality (`ushort` read through `int`)
- `52`: quality detail (`uint`)
- `56`: OPC quality (`ushort` read through `int`)
- `72`: string value (`std::wstring`)
- `104`: numeric value (`double`)
- `128`: percent good (`double`)
- `336`: start timestamp (`FILETIME`)
- `400`: interpolation/timestamp/quality packed flags (`ushort`)
- `416`: source tag (`std::wstring`)
- `448`: source server (`std::wstring`)
- `488`: actual resolution in FILETIME ticks
## Enum Values Mirrored Into The .NET 10 Scaffold
`HistorianRetrievalMode`:
- `Cyclic = 0`
- `Delta = 1`
- `Full = 2`
- `Interpolated = 3`
- `BestFit = 4`
- `TimeWeightedAverage = 5`
- `MinimumWithTime = 6`
- `MaximumWithTime = 7`
- `Integral = 8`
- `Slope = 9`
- `Counter = 10`
- `ValueState = 11`
- `RoundTrip = 12`
- `StartBound = 13`
- `EndBound = 14`
Other confirmed enums:
- `HistorianInterpolationType`: `StairStep = 0`, `Linear = 1`,
`SystemDefault = 254`, `None = 255`
- `HistorianTimestampRule`: `Start = 0`, `End = 1`, `None = 2`
- `HistorianQualityRule`: `Extended = 0`, `Good = 1`, `None = 2`,
`Optimistic = 3`
- `HistorianValueSelector`: `Auto = 1`, `First = 2`, `Last = 3`,
`Integral = 4`, `StdDev = 5`, `Minimum = 6`, `Maximum = 7`,
`Average = 8`
- `HistorianAggregationType`: `Minimum = 0`, `Maximum = 1`, `Average = 2`,
`Total = 3`, `Percent = 4`, `MinContained = 5`, `MaxContained = 6`,
`TotalContained = 7`, `AverageContained = 8`, `PercentContained = 9`
## Retrieval Metadata Contract
Decompiled WCF retrieval contract signatures in `aahClientManaged.dll` match
the .NET 10 scaffold for the scalar and byte-buffer metadata calls:
- `GetTagTypeFromName(uint clientHandle, string tagName, out uint tagType)`
- `IsOriginalAllowed(uint clientHandle, out bool isAllowed)`
- `IsManualTag(uint clientHandle, string tagName, out bool isManual)`
- `GetTagInfoFromName(uint clientHandle, string tagName,
out uint tagMetadataByteCnt, out byte[] tagMetadata)`
- `GetTagInfosFromId(uint handle, uint tagIdsSize, byte[] tagIds,
ref uint sequence, out uint tagInfosSize, out byte[] tagInfos)`
- `GetTagInfosFromName(uint handle, uint tagNamesSize, byte[] tagNames,
ref uint sequence, out uint tagInfosSize, out byte[] tagInfos)`
Remote managed evidence against `10.100.0.48:32568` confirms the integrated
session handle works for scalar retrieval calls. For
`OtOpcUaParityTest_001.Counter`, `GetTagTypeFromName` returns type `1`,
`IsManualTag` returns `false`, and `GetTagInfoFromName` returns `238` with no
metadata bytes. `GetTgByNm` and `GetTg` buffer probes currently return the same
`238` and no output bytes for all tried encodings. That keeps exact metadata
buffer construction unresolved.
`ArchestrA.TagQuery.StartQuery` is a separate tag browse/query path. It calls
`HistorianClient.StartTagQuery` with a UTF-16 tag-filter pointer, an output
query handle, an output tag count, and `SError*`. The WCF continuation in
`CRetrievalConnectionWCF.StartTagQuery` serializes a request buffer before
calling `IRetrievalServiceContract3.StartTagQuery(handle, byte[], out byte[],
out byte[])`.
IL instrumentation of `CRetrievalConnectionWCF.StartTagQuery` token `06004A15`
captured the exact WCF request buffer for deterministic OData filter
`TagName eq 'OtOpcUaParityTest_001.Counter'`. The request is 92 bytes with
SHA-256 `af1dbcdd3eb0ad91a18882c22252aa74aff82998e96a39b63415ab4792a962ac`;
sanitized field notes are stored in `starttagquery-request-buffer-latest.json`.
Confirmed tag-query request fields:
- offset `0x00`: `ushort` marker `26449` (`0x6751`)
- offset `0x02`: `ushort` version `1`
- offset `0x04`: `uint32` filter character length
- offset `0x08`: UTF-16LE filter text without a trailing null
The same instrumentation also captures the successful WCF response byte array.
For the current local run the response is 8 bytes with SHA-256
`db49223a2cf9616171322e5325816a7a579582ebdce91c2f89df8df7aa8aac01`.
Confirmed start-tag-query response fields:
- offset `0x00`: `uint32` query handle
- offset `0x04`: `uint32` tag count
The local native tag query succeeds with that OData filter and returns
`TagCount = 1`, tag name
`OtOpcUaParityTest_001.Counter`, and metadata summary `TagKey = 238`,
`TagDataType = 4`, `TagStorageType = 3`, `EngineeringUnit = None`. Plain tag
name filters reach the server but fail with native failure code `1`, while
wildcard filters fail as invalid OData/WWFilter syntax. Response buffers and
the native `CTagMetadata` vector layout for `GetTagInfo` still need byte-level
instrumentation.
`TagQuery.GetTagNames` calls `HistorianClient.GetTagNames`, while
`TagQuery.GetTagInfo` calls `HistorianClient.GetTagInfos` and then
`HistorianTag.LoadFromTagMetadata` for each 224-byte `CTagMetadata` entry in
the returned native vector. The separate wildcard-style browse path is not
`TagQuery`; method inventory shows it as
`HistorianClient.StartLikeTagNameSearch` / `GetLikeTagnames` with WCF
continuations `CRetrievalConnectionWCF.StartLikeTagNameSearch` /
`GetLikeTagnames`.
IL instrumentation of `aahClientCommon.CClientCommon.GetTagInfos` token
`06002EC9` captured the pre-deserialization response stream used by
`TagQuery.GetTagInfo`. For the deterministic tag
`OtOpcUaParityTest_001.Counter`, the stream is 106 bytes with SHA-256
`77b2bf720d8888f08a1499a8162e706c2cef567a1f6d74d7e92efe0cd3e3e34b`;
sanitized details are stored in `tagquery-gettaginfo-response-latest.json`.
Confirmed `GetTagInfo` response stream fields for the current fixture:
- offset `0x0000`: `uint32` tag count
- offset `0x0004`: 4-byte native data-type descriptor; `CTagUtil.GetDataValueType`
maps this fixture to public `HistorianDataType = 4`
- offset `0x0008`: type GUID
- offset `0x0018`: `uint32` tag key
- offset `0x001C`: compact ASCII tag name: marker `0x09`, `uint16` byte length,
then bytes
- offset `0x003C`: compact ASCII metadata provider, observed as `MDAS`
- offset `0x0043`: native tag class byte
- offset `0x0044`: storage type byte
- offset `0x0045`: deadband type byte
- offset `0x0046`: interpolation type byte
The same instrumentation captured the native `std::vector<CTagMetadata>` after
`Load<CTagMetadata,SByteStream<SCrtMemFile>>`; the vector has 224-byte elements.
`HistorianTag.LoadFromTagMetadata` reads tag key from vector offset `0x0030`,
tag name from `0x0038`, description from `0x0058`, storage type from `0x00C1`,
deadband type from `0x00C2`, and inactive status from vector offset `0x0034`.
The wildcard browse path is confirmed separately through managed WCF calls to
`Retr.StartLikeTagNameSearch` and `Retr.GetLikeTagnames`. With integrated
Windows `Open2` and filter `OtOpcUaParityTest%`, `StartLikeTagNameSearch`
returns `0`, and `GetLikeTagnames` returns one 66-byte buffer with SHA-256
`2d450a55f392aed0026e9a957fefa3b116aab6ec81912c5d824c6b9a1ff5a4a1`.
The buffer layout for the one-tag fixture is `uint32 count`, then for each tag
`uint32 UTF-16 character length` and UTF-16LE tag name bytes. Sanitized details
are stored in `like-tag-browse-response-latest.json`.
## Event Query Contract
`EventQuery.StartQuery` calls native `HistorianClient.StartEventQuery`.
Confirmed arguments include:
- Start/end timestamps converted to FILETIME.
- Event count.
- Skip count.
- Event order as `ushort`.
- Filter block pointer.
- Time zone string: `UTC`.
- Output query handle.
The decompiled wrapper passes event query type value `1` into
`HistorianClient.StartEventQuery` for ordinary event reads. This is distinct
from the WCF retrieval `queryRequestType`: `Query.StartEventQuery` stores
`ushort 3` immediately before calling
`CRetrievalConnectionWCF.StartEventQuery`, so the WCF operation must use
`queryRequestType = 3`.
`EventQueryRequest.Save<SByteStream<SCrtMemFile>>` now has a byte-level field
order from `ildasm`:
- constant request version `ushort 5`
- start and end FILETIME values from the embedded `TIMERANGE`
- `eventCount` as `uint32`
- `skipCount` as `uint32`
- `eventOrder` as `uint16`
- `queryType` as `uint16`
- `EventQueryFilters.Save`
- result buffer size as `uint32`; native construction passes `0x10000`
- timezone `std::wstring`; ordinary reads pass `UTC`
- `CMetadataNamespace.Save`
- `EventQueryFilters.SaveSelect`
`EventQueryFilters.Save` writes `ushort 0`, a `uint32` filter count, each
filter, a continuation flag byte, and only writes continuation FILETIME/GUID
when that flag is non-zero. Empty event reads therefore serialize the filter
block as `00 00 00 00 00 00 00`. `SaveSelect` writes a `uint32` selected
property count followed by compressed strings; default event reads write count
zero.
`EventQuery` allocates `EventQueryFilters` as a 72-byte native object. It rejects
filters on `EventTime`, limits filter count to 500, and converts filter property
names and values to UTF-16 strings.
`EventQuery.MoveNext` calls `HistorianClient.GetNextRow<EventQueryResultRow>` and
then `HistorianEvent.CreateHistorianEvent`.
Integrated-auth native harness event evidence
(`native-trace-harness-event-latest.json`) confirms:
- Opening with `HistorianConnectionType = Event` succeeds and reaches
`ConnectedToServer = true`.
- `HistorianAccessUtil.SetConnectionMode(Event, integratedSecurity: true)`
produces Open2 connection mode `0x501` (`1281`). The process read-only
integrated mode is `0x402` (`1026`).
- Native `HistorianAccess.OpenConnection` creates the event connection at
`ConnectionIndex.Event` and then calls `CreateDefaultEventTag`.
`CreateDefaultEventTag` builds a synthetic `HistorianTag` named
`CM_EVENT`, sets description `AnE Event`, engineering unit `NONE`, data type
`10`, and calls `AddTagInternal`; the native
access snapshot then has `clientHandlerArray = [0, 1]` and
`eventTagHandle = 10000000`.
- `EventQueryArgs` uses UTC `StartDateTime` / `EndDateTime`, `QueryType =
Events`, `EventOrder = Ascending`, and `EventCount` from `--max-rows`.
- `EventQuery.StartQuery` succeeds with native `Success`.
- A seven-day local-dev window returned three sanitized event rows; raw event
IDs and opaque event source details are not written by the harness.
- A managed WCF `StartEventQuery` probe using this reconstructed version-5
empty-filter request and `queryRequestType = 3` reaches
`/Retr.StartEventQuery` but returns `false` with no native error buffer. This
remains true for process-mode Open2 (`1026`) and event-mode Open2 (`1281`),
and for the returned Open2 handle, the native `ClientApp` event slot handle
`1`, and the observed event tag handle `10000000`.
- `CRetrievalConnectionWCF.StartEventQuery` has a server-interface-version
branch: interface versions `<= 2` call `/Retr.StartQuery2`; newer versions
call `/Retr.StartEventQuery`. The managed probe now tries both operations
with the same reconstructed request. On the local 2020 server, both operation
paths still return `false` with no native error buffer, so the mismatch is
likely connection/session state rather than this dispatch branch.
- Because native event open performs `CreateDefaultEventTag` before event query
start, the next gap is likely the `HistorianClient.AddHistorianTag` /
`AddTagInternal` setup for `CM_EVENT`, not the basic event request field
order or WCF contract signature.
- Follow-up decompile of `aahClientCommon.CClient.RegisterTag` shows this part
is client-local: it calls `RegisterRealtimeTag`, inserts the tag id into
local sets, marks the cached `CTagInfo` as registered, and flips
synchronization flags. There is still a server-visible registration path
elsewhere (`IHistoryServiceContract.RTag` / storage `RTag`), but
`CreateDefaultEventTag` does not directly prove its byte payload.
- The server-visible `RTag2` WCF edge is now partially mapped. Decompiled
`CHistoryConnectionWCF.RegisterTags` converts a native `_GUID Handle` to a
string, calls `IHistoryServiceContract2.RegisterTags2` (`RTag2`), and passes a
byte buffer serialized as `uint32 count` followed by `count` 16-byte `_GUID`
values. The local managed probe `wcf-register-event-tag` tried empty,
zero-GUID, and event-handle-shaped GUID vectors with Open2-derived handle
candidates. All failed before event query start could succeed: first-16
handle candidates returned native error `51`, while the bytes 4-19 .NET GUID
candidate reached the server but returned native error `1`. This makes simple
`RTag2` replay unlikely to be the missing event setup by itself; the richer
`AddHistorianTag`/`CTagMetadata` path remains the lead.
- `HistorianClient.ConvertEventTagToTagMetadata` now has a concrete event-tag
metadata map from IL/native data:
- synthetic tag name: `CM_EVENT`
- description from `CreateDefaultEventTag`: `AnE Event`
- event tag id: `353b8145-5df0-4d46-a253-871aef49b321`
- common ArchestrA event type id:
`5f59ae42-3bb6-4760-91a5-ab0be01f2f27`
- ArchestrA event type id:
`f3ef1a17-fd27-4c3c-84cf-bac55c0e47de`
- converted `CDataType` byte: `5`
- storage type passed to `CTagMetadata.Initialize`: `2`
- `CTagMetadata.Save<SByteStream<SCrtMemFile>>` writes a one-byte serialization
version, a two-byte optional-field mask, the `CDataType` byte, the 16-byte tag
id, and compressed strings for present string fields. Short ASCII UTF-16
strings use the compact form `09 <length> 00 <ASCII bytes>`, so `CM_EVENT`
serializes as `09 08 00 43 4d 5f 45 56 45 4e 54`.
- A direct managed replay of this approximate event metadata through
`IHistoryServiceContract.AddTags` (`AddT`) is negative evidence:
`wcf-add-event-tag` opened `/Hist-Integrated`, tried both a raw
`CTagMetadata` payload and a vector-count-prefixed payload, and both returned
`AddReturnCode = 4` with no output buffer. The subsequent
`StartEventQuery` still returned `false`. This means the missing native event
setup is not reproduced by the current `AddT` approximation.
- A fake WCF capture server on an alternate port exposed `/Hist`,
`/Hist-Integrated`, and `/Retr`, but the native event harness still reached
the real local Historian and the fake server saw no calls. The
`HistorianConnectionArgs.TcpPort` field does not redirect this native WCF
path sufficiently for server-side capture.
- The expanded Frida event add-tag pass hooked the candidate MethodDef RVAs
before `OpenConnection` and the native event query still succeeded, but no
enter/leave callbacks fired. MethodDef RVAs remain insufficient hook targets
for this mixed-mode wrapper; use CLR method rewriting/profiling or lower-level
transport/API capture next.
- IL instrumentation of `<Module>.Query.StartEventQuery` token `0600574A`
now captures the exact native `EventQueryRequest` buffer immediately after
`EventQueryRequest.Save<SByteStream<SCrtMemFile>>` and `endstream`. A local
seven-day event query succeeded and returned three rows while logging a
65-byte request. SHA-256 is
`6b955b02087047a3199a8c74f3eee85c3b49aaa29b05de12eff2dd536f2da0d5`, and
the sanitized dump is stored in `starteventquery-request-buffer-latest.json`.
The managed event query serializer now has a byte-for-byte test against this
native fixture.
- A matching IL rewrite of
`<Module>.HistorianClient.GetNextRow<class EventQueryResultRow>` token
`06005965` captures native `EventQueryResultRow*` memory immediately after
`Query.GetNextEventQueryResultRow<class EventQueryResultRow>` returns. The
successful local seven-day event query emitted three 2048-byte raw snapshots
under ignored `artifacts\reverse-engineering`; the sanitized summary is
stored in `getnexteventrow-memory-latest.json`.
Confirmed first-pass event row evidence:
- offset `0x18`: managed `HistorianEvent.EventTime` as FILETIME UTC for all
three rows
- `ReceivedTime` was not present as a direct FILETIME value in the captured
2048-byte native struct snapshot
- managed event type was `User.Write.Secured` for all three rows, but the raw
struct contains pointer-like fields and no safely decoded inline event-type
string yet
This keeps event result parsing unresolved, but it proves the IL rewrite path
can capture both event request bytes and native event row memory without adding
production dependencies.
## Tag Query Session Evidence
`CRetrievalConnectionWCF.StartTagQuery` does not carry the tag filter in the
latest successful WCF call. Combined IL instrumentation of native
`OpenConnection` plus tag-query start captured a successful local tag metadata
query where the WCF `StartTagQuery` request was only:
- marker/version bytes: `51 67 01 00`
- SHA-256:
`17956e4fbe53d5edc0f9170203b013432e4afcc0591c795a10522a98d9fce926`
- response bytes: `0C 00 00 00 01 00 00 00`
- parsed response: query handle `12`, tag count `1`
The tag filter `TagName eq 'OtOpcUaParityTest_001.Counter'` was already
processed before this WCF request. The native WCF handle string is an uppercase
GUID generated for the native retrieval session; it is not the numeric Open2
handle, the Open2 storage session id, the first 16 Open2 bytes, or the full
Open2 output. Managed replay of both the older 92-byte filter-shaped candidate
and the latest 4-byte header-only request with all Open2-derived handle
candidates returned native errors `51` or `1`.
Conclusion: public metadata should not be wired through `StartTagQuery`.
`BrowseTagNamesAsync` can stay on the proven `StartLikeTagNameSearch` /
`GetLikeTagnames` path. `GetTagMetadataAsync` now uses the direct
`GetTagInfoFromName` WCF path, which returned a 98-byte metadata record for the
deterministic parity tag. More guessed `StartTagQuery` envelopes are not the
useful next target; the missing evidence is the native session registration or
the earlier metadata-layer call that binds the filter to the retrieval GUID.
## Immediate Unknowns
These still require native ABI inspection and packet captures:
- Session handshake frame.
- Authentication frame.
- Query frame envelope and length/correlation fields.
- Native `StartDataQuery` message type identifier.
- Native `StartEventQuery` message type identifier.
- `GetNextRow` request/response frame shape.
- Error frame layout.
See `native-exports.md` for the first PE export-table pass over
`aahClient.dll`.