Files
histsdk/docs/reverse-engineering/grpc-tag-query-odata.md
T
Joseph Doherty 630295bd18 docs: QueryTag native-RE attempt — lightweight tooling insufficient, needs Ghidra
Recorded the native-disassembly attempt on aahClientManaged.dll (mixed-mode):
ilspycmd cannot decompile it; capstone byte-search can't locate the StartTagQuery
0x6751 marker (not a plain immediate — it's an .rdata constant loaded RIP-relative,
the .text "51 67 00 00" hits are coincidental jump-table data). Managed metadata
gives QueryTag field semantics but not the binary packet-id. Finishing QueryTag
needs Ghidra/IDA xref analysis or the live IL-rewrite capture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
2026-06-21 15:46:54 -04:00

4.6 KiB

R0.1 browse over gRPC — StartTagQuery takes an OData filter (2026-06-21)

Live-probed RetrievalService.StartTagQuery / QueryTag against a real 2023 R2 server over the gRPC front door (string-handle = uppercase Open2 storage GUID). Key result: browse is feasible on 2023 R2 — the 2020 WCF "metadata-server pipe" wall does not block here.

StartTagQuery — CRACKED

StartTagQuery(strHandle, btRequest) where btRequest = the native marker(26449) + version(1) + WriteHistorianString(filter) buffer (HistorianTagQueryProtocol.CreateStartTagQueryAttempt). The server runs CMdServer::StartTagQuery::StartActiveTagnamesQuery over \\.\pipe\aahMetadataServer\console and parses the filter string as OData (not SQL-LIKE). Swept filters:

filter result
startswith(TagName,'Sys') success, 8-byte response
contains(TagName,'Sys') success
TagName eq 'SysTimeSec' success
`` (empty) success (all tags)
Sys* / * ODataFilter ... bad token
TagName like 'Sys%' / Name like 'Sys%' rejected

Success response btResponse is the 8-byte (queryHandle:uint, tagCount:uint) pair (ParseStartTagQueryResponse). Live: startswith(TagName,'Sys') → tagCount = 220.

Implication for the public API: browse must translate the SDK's glob filter to OData — * → empty, Pre*startswith(TagName,'Pre'), *sub*contains(TagName,'sub'), exact → TagName eq '...'. (Escaping single-quotes in names still TBD.)

QueryTag — OPEN (needs native RE of aahClient.dll)

QueryTag(strHandle, uiQueryHandle, btRequest) is the paging call that returns the tag-name rows. Every btRequest shape tried returns the constant native error type 4 / code 72 = InvalidPacketId (ArchestrA.CloudHistorian.Contract.ErrorCode.InvalidPacketId = 72; the empty buffer alone gives a different code). So the btRequest must carry a packet-id header specific to QueryTag that we don't have — the generic 0x6751/version-1 header (which StartTagQuery accepts) is rejected here.

Semantic fields are known from ArchestrA.CloudHistorian.Contract:

  • request QueryTagRequest = QueryHandle:uint("q") + QueryType:ushort("t") + StartIndex:uint("s") + TagCount:uint("c")
  • response QueryTagResponse = QueryHandle:uint + TagNames:string[]("t") + NextIndex:uint("i") + TagMetadataBuffer:byte[]("tb") — so QueryTag returns the names directly (plus an optional metadata buffer), and pages via StartIndex/NextIndex.

What's missing is the binary btRequest packet framing (the QueryTag packet id + how those fields are laid out). That serializer lives in native aahClient.dllaahClientManaged.dll is mixed-mode (C++/CLI) so ilspycmd cannot decompile it, and no managed assembly builds the buffer. Completing QueryTag therefore requires native RE (Ghidra/IDA on aahClient.dll) or a live gRPC capture of the stock 2023 R2 client browsing. Do not ship a guessed QueryTag request (project discipline: no guessed wire bytes).

Probe helpers live in Grpc/HistorianGrpcTagClient (ProbeStartTagQuery, ProbeTagQuerySequence) and the gated StartTagQuery_OverGrpc_AcceptsODataFilter test pins the StartTagQuery+OData result.

Native-RE attempt (2026-06-21) — needs Ghidra/IDA

The QueryTag packet-id is built in native code (the C++ HCAL inside the mixed-mode aahClientManaged.dll). Lightweight tooling was exhausted and is insufficient:

  • ilspycmd cannot decompile or even list the mixed-mode assembly (throws).
  • capstone byte-search: the StartTagQuery marker 0x6751 is not a plain mov immediate in .text (no 66 C7 … 51 67 and no clean B8 51 67 00 00); the three 51 67 00 00 hits in .text are coincidental jump-table data (disassembly around them is garbage). The constant lives in the .rdata pool (which holds the 51 67 bytes) and is loaded RIP-relative — so the serializer can't be found by immediate-scanning without cross-reference analysis.
  • Managed metadata (ArchestrA.CloudHistorian.Contract) gives the field semantics — QueryTagRequest{SessionId, QueryHandle:uint, QueryType:ushort, StartIndex:uint, TagCount:uint}, DataContract code QTLQ (StartTagQuery = TLQR) — but not the binary packet-id/framing.

Finishing QueryTag therefore requires Ghidra/IDA (decompiler + xref to the .rdata marker constant to find the serializer and read the QueryTag packet-id + byte layout), or the live capture option (IL-rewrite Archestra.Historian.GrpcClient.QueryTag to log requestBuffer while a real 2023 R2 client browses). StartTagQuery (the hard OData part) remains cracked and live-verified.