R0.1 browse over gRPC SHIPPED — QueryTag cracked, M0 gRPC parity complete
Wires HistorianClient.BrowseTagNamesAsync over gRPC (Transport==RemoteGrpc) via
Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync: StartTagQuery(OData) -> paged
QueryTag -> EndTagQuery. Live-verified against a real 2023 R2 server (returns Sys* tags).
QueryTag packet-id recovered WITHOUT native disassembly: a .rdata packet-descriptor
table in aahClientManaged.dll lists {0x6751,1}=StartTagQuery immediately followed by
{0x6752,1}=QueryTag (found via pefile byte-scan of .rdata), confirmed live.
Wire format (live-verified):
- request btRequest = u16 0x6752 + u16 version(1) + u16 queryType(1=names) + u32 startIndex + u32 count
- response btResonse = u32 count + per-name(u32 charCount + UTF-16LE) + trailer (NextIndex/metadata, ignored)
- new HistorianTagQueryProtocol.ParseTagNameQueryPage tolerates the trailer
- GlobToODataFilter translates the SDK glob filter to OData (Pre*->startswith, *suf->endswith,
*mid*->contains, exact->eq); the 2023 R2 metadata-server parses filters as OData.
Replaces the earlier RE probe helpers with the shipped browse path. Adds golden-byte
(BuildQueryTagRequest) + 8 glob-translation unit tests + gated live browse test.
226 unit tests pass; 5/5 live gRPC tests pass (read, probe, system-param, metadata, browse).
Milestone 0 (full gRPC parity) is complete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
@@ -28,45 +28,30 @@ Success response `btResponse` is the 8-byte `(queryHandle:uint, tagCount:uint)`
|
||||
`*` → 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 — CRACKED (2026-06-21), browse SHIPPED
|
||||
|
||||
`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.
|
||||
The blocker was the packet id: every guessed `btRequest` returned native error **type 4 / code 72 =
|
||||
`InvalidPacketId`** (`ArchestrA.CloudHistorian.Contract.ErrorCode.InvalidPacketId`). The generic
|
||||
`0x6751` header that StartTagQuery accepts is the **wrong** id for QueryTag.
|
||||
|
||||
**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.
|
||||
**How it was found (no Ghidra needed):** a `.rdata` **packet-descriptor table** in
|
||||
`aahClientManaged.dll` lists consecutive `{uint marker, uint version}` entries —
|
||||
`{0x6751, 1}` (StartTagQuery) immediately followed by **`{0x6752, 1}`** (the paired op). Found by
|
||||
`pefile` byte-scan of `.rdata` for `51 67 00 00` and dumping the surrounding dwords. Testing `0x6752`
|
||||
live confirmed it.
|
||||
|
||||
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.dll`** — `aahClientManaged.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).
|
||||
**QueryTag wire format (live-verified):**
|
||||
- request `btRequest` = `u16 marker(0x6752) + u16 version(1) + u16 queryType + u32 startIndex + u32 count`
|
||||
— `queryType = 1` returns tag-name rows (`queryType = 0` returns an empty/count-only page).
|
||||
- response `btResonse` = `u32 count + per-name(u32 charCount + UTF-16LE) + trailer`
|
||||
(the trailer is the CloudHistorian `NextIndex`/`TagMetadataBuffer` region — ignored by
|
||||
`HistorianTagQueryProtocol.ParseTagNameQueryPage`).
|
||||
- Semantic fields match `ArchestrA.CloudHistorian.Contract.QueryTagRequest`
|
||||
(`QueryType/StartIndex/TagCount`; the QueryHandle travels in the protobuf `uiQueryHandle`).
|
||||
|
||||
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.
|
||||
**Browse is shipped:** `HistorianClient.BrowseTagNamesAsync` routes over gRPC when
|
||||
`Transport==RemoteGrpc` via `Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync`
|
||||
(StartTagQuery(OData) → paged QueryTag(0x6752) → EndTagQuery), with the SDK glob filter translated by
|
||||
`GlobToODataFilter`. Golden-byte + glob unit tests and a gated live test
|
||||
(`BrowseTagNamesAsync_OverGrpc_ReturnsSystemTags`) cover it. **M0 gRPC parity is complete.**
|
||||
|
||||
Reference in New Issue
Block a user