From 34e352ba2877e00846cf4b718c6243b7f9a3a7b1 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 20 Jun 2026 16:11:35 -0400 Subject: [PATCH] R1.8/R1.9: empirical summary-query probe + enum-dump RE command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pushed on recovering the summary query params. Findings: - Added `enum-dump` to the RE CLI (dumps a managed enum's literal members). Confirmed INSQL_QUERYTYPE / HISTORIAN_SUMMARYTYPE are value__-only in the managed metadata — their named members are native-side constants, so they can't be recovered statically. Same for CColumnNameMap.LoadColumnNameMap (column->bit built from native string/const data, not IL ldstr/ldc). - Live-probed StartQuery2 against SysTimeSec sweeping QueryType/SummaryType/ ColumnSelectorFlags. The server ACCEPTS SummaryType 1/2/4/5 (returns a valid version-9 buffer) but yields 0 rows; column flags don't change that; QueryType 15/16 don't exist. So a summary query is NOT Full+SummaryType+ flags — the config lives in the AutoSummaryParameters trailer (currently zeroed) and/or a native summary QueryType. Conclusion recorded in the plan: the request shape needs a NATIVE capture (instrument-wcf-writemessage on a real summary query), not managed-metadata recovery or blind probing. Decode targets remain located. No guessed code in src/; probe scaffolding removed. 208 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/plans/r1.8-r1.9-summary-queries.md | 26 ++++++++++ .../Program.cs | 48 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/docs/plans/r1.8-r1.9-summary-queries.md b/docs/plans/r1.8-r1.9-summary-queries.md index 0c9b704..92b03ba 100644 --- a/docs/plans/r1.8-r1.9-summary-queries.md +++ b/docs/plans/r1.8-r1.9-summary-queries.md @@ -34,6 +34,32 @@ Found via `methods … Summary` + `dnlib-method`: | `CTypeMetadata.IsAnalogSummary` / `IsStateSummary` | `0x060001A4/A5` | server-side type gating | | `INSQL_QUERYTYPE` / `HISTORIAN_SUMMARYTYPE` | enums `0200013F` / `02000191` | the `QueryType` / `SummaryType` values to send | +## Empirical probe results (2026-06-20, live `SysTimeSec` over `StartQuery2`) + +Swept `QueryType`/`SummaryType`/`ColumnSelectorFlags` against the live 2020 server: + +- `QueryType=2 (Full)`, `SummaryType ∈ {0,3,6}` → normal 109-byte version-9 data buffer. +- `QueryType=2`, `SummaryType ∈ {1,2,4,5}` → **valid version-9 buffer with 0 rows** (`09 00 00 00 00 00`). + The server **accepts** these summary types but yields no rows. +- The 0-row result is **unchanged** by `ColumnSelectorFlags` (tried default, all-bits + `0xFFFF…FFFF`, high-dword, low-48). So column flags are *not* the unlock. +- `QueryType ∈ {15,16}` → `GetNext` blocks/times out (no such INSQL_QUERYTYPE ordinal). + +**Conclusion:** a summary query is *not* `Full + SummaryType + column flags`. The summary +configuration lives elsewhere in the request — almost certainly the **`AutoSummaryParameters` +trailer** (`SerializeFullHistoryRequest` currently writes it all-zero via +`WriteAutoSummaryParameters`) and/or a native summary `QueryType`. Both are **native-side +constants** (`HISTORIAN_SUMMARYTYPE` / `INSQL_QUERYTYPE` are `value__`-only in managed metadata; +`CColumnNameMap.LoadColumnNameMap` builds column→bit via native string/const data, not IL +`ldstr`/`ldc`). So they cannot be recovered from managed metadata, and blind probing of the +obvious fields returns empty. + +**Therefore the right next step is a native request capture, not more probing:** drive the native +client (NativeTraceHarness / a real summary query) and capture the `pRequestBuff` bytes via the +existing `instrument-wcf-writemessage` pipeline — the same method that produced every other proven +request shape (reads, events, EnsT2). Diff that buffer against a normal Full request to read off +the exact `QueryType` + `SummaryType` + `AutoSummaryParameters` layout, then implement against it. + ## Open questions (nail these next, in order) 1. **Request params.** Recover the exact `QueryType` + `SummaryType` (+ whether `ColumnSelectorFlags` diff --git a/tools/AVEVA.Historian.ReverseEngineering/Program.cs b/tools/AVEVA.Historian.ReverseEngineering/Program.cs index 014cd6b..2b64c15 100644 --- a/tools/AVEVA.Historian.ReverseEngineering/Program.cs +++ b/tools/AVEVA.Historian.ReverseEngineering/Program.cs @@ -35,6 +35,7 @@ try "dnlib-method" => InspectDnlibMethod(args), "method-refs" => PrintMethodReferences(args), "field-constant" => PrintFieldConstantReferences(args), + "enum-dump" => DumpEnumMembers(args), "instrument-startdataquery" => InstrumentStartDataQuery(args), "instrument-getnextrow" => InstrumentGetNextRow(args), "instrument-wcf-readquery" => InstrumentWcfReadQuery(args), @@ -80,6 +81,53 @@ catch (Exception ex) return 1; } +static int DumpEnumMembers(string[] args) +{ + string path = args.Length > 1 ? args[1] : Path.Combine("current", "aahClientManaged.dll"); + string filter = args.Length > 2 ? args[2] : throw new ArgumentException("Usage: enum-dump [dll-path] [type-name-substring]"); + + dnlib.DotNet.ModuleDefMD module = dnlib.DotNet.ModuleDefMD.Load(path); + int matchedTypes = 0; + foreach (dnlib.DotNet.TypeDef type in module.GetTypes()) + { + if (type.Name.String.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) + { + continue; + } + + matchedTypes++; + Console.WriteLine($"Type: {type.FullName} (enum={type.IsEnum}, fields={type.Fields.Count})"); + foreach (dnlib.DotNet.FieldDef field in type.Fields) + { + object? value = field.Constant?.Value; + if (value is null) + { + Console.WriteLine($" {field.Name,-40} [literal={field.IsLiteral} static={field.IsStatic} sig={field.FieldType?.TypeName}] (no const)"); + continue; + } + string hex = value switch + { + int i => $"0x{i:X}", + uint u => $"0x{u:X}", + short s => $"0x{s:X}", + ushort us => $"0x{us:X}", + long l => $"0x{l:X}", + byte b => $"0x{b:X}", + sbyte sb => $"0x{sb:X}", + _ => "?" + }; + Console.WriteLine($" {field.Name,-40} = {value} ({hex})"); + } + } + + if (matchedTypes == 0) + { + Console.WriteLine($"No type matched '{filter}'."); + } + + return 0; +} + static int PrintExports(string[] args) { string path = args.Length > 1 ? args[1] : Path.Combine("current", "aahClient.dll");