Query-farm/vgi-symbols

GitHub: Query-farm/vgi-symbols

一个 DuckDB 的原生符号化 worker,通过 SQL 将百万级堆栈帧批量解析为函数、文件和行号,并以持久缓存消除调试信息的重复解析。

Stars: 0 | Forks: 0

VGI

# vgi-symbols **原生符号化即 SQL JOIN** — 将一列 `(build_id, address)` 堆栈帧解析为 `function` + `file` + `line` 以及 **内联调用链**, 通过精确解析原生调试信息(ELF / Mach-O / dSYM 的 DWARF,Windows PDB), 就像 `addr2line` / `llvm-symbolizer` 所做的那样,并由一个 **持久的、以 build-id 为键的 调试信息缓存** 作为支撑,使得数以百万计的帧能够在引擎内进行符号化,而每个调试模块**只需解析一次** — 无需外部符号化服务,无需逐行重新解析,也无需将崩溃数据发送至 SaaS。 一个用于 DuckDB 的 [VGI](https://query.farm) worker。这是崩溃/性能分析浪潮的 **解析后端**:[`vgi-minidump`](https://query.farm)、 [`vgi-pprof`](https://query.farm) 和 [`vgi-perf`](https://query.farm) 提取 `(build_id, address)` 帧并将其输入至此。 ## 为什么缓存是核心产品 *一旦模块被解析*,解析一个地址是非常廉价的 — 在已排序的行表中进行二分查找并遍历内联树。解析模块则**不然**:一个发布版的 `libchrome.so.debug` 或游戏引擎的 PDB 是数百 MB 的 DWARF/PDB。 简单的 `addr2line` 逐行处理会为**每个**帧重新解析它;在整个性能分析集群中,这就是全部的成本所在。缓存将 `O(frames × parse)` 转化为 `O(modules × parse + frames × lookup)`。**这正是该 worker 的核心价值。** 缓存以格式规范化的 **debug-id** 作为键(ELF GNU build-id → GUID+age,Mach-O `LC_UUID`,PDB GUID+age),因此一列混合平台的帧可以通过一个缓存进行解析。它具有大小限制(按字节数和模块数的 LRU),对被证实缺失的 build-id 设有 **负缓存**,并带有一个支持序列化的 **清单**(源索引 + 来源信息 + 负缓存),该清单能够跨批次边界存续并驱动冷启动。 ## 快速开始 ``` INSTALL vgi FROM community; LOAD vgi; ATTACH 'symbols' AS symbols (TYPE vgi, LOCATION '/path/to/symbols-worker'); -- Point the worker at where debug files live (LOCAL by default; zero egress). CALL symbols.add_source('dir', path => '/srv/debug'); CALL symbols.add_source('glob', path => '/builds/**/*.{debug,pdb,dSYM}'); -- Resolve a single (build_id, address) frame to function/file/line + inline chain. SELECT symbols.main.symbolicate('76ff8518da153e64a8403892fcbf11250', 4294968116) AS frame; -- → {function: 'apply', file: '…/prog.c', line: 16, -- inlined: [{function:'inner_lo', …}, {function:'inner_hi', …}], status: 'ok', …} ``` ## SQL 接口 ``` -- 1. Resolve one frame to a STRUCT with the inline chain collapsed (scalar). SELECT symbols.main.symbolicate(build_id, address) FROM crash_top_frames; -- 2. Just a label (the fast path for a flamegraph leaf / GROUP BY key). SELECT symbols.main.function_name(top.build_id, top.address) AS crash_site, count(*) AS hits FROM crash_top_frames top GROUP BY crash_site ORDER BY hits DESC; -- "which function are we crashing in" -- 3. THE WORKLOAD: bulk-symbolicate a frame table, fanning each address out to -- its inline chain via the scalar struct + UNNEST (innermost-first). SELECT f.thread_id, f.frame_idx, u.depth, u.fr.function, u.fr.file, u.fr.line FROM stack_frames f, UNNEST(list_transform( symbols.main.symbolicate(f.build_id, f.address).inlined, (x, i) -> {'depth': i, 'fr': x})) AS t(u) ORDER BY f.thread_id, f.frame_idx, u.depth; -- 4. Just the inline chain at an address. SELECT symbols.main.inline_frames(build_id, address) FROM frames; -- 5. Demangle a raw symbol (Itanium C++ / Rust legacy+v0 / MSVC / Swift). SELECT symbols.main.demangle('_ZN3foo3barEv'); -- → 'foo::bar' -- 6. Inspect a module before resolving: is it usable, and does its debug-id match? SELECT * FROM symbols.main.module_info('/srv/debug/libssl.so.debug'); -- → format MachO/ELF/PE/PDB, build_id, debug_id, arch, has_dwarf, has_line_table, symbol_count -- 7. Cache observability (the stateful part) — what is parsed and resident now. SELECT debug_id, name, format, bytes_resident, rows_resolved, last_used FROM symbols.main.cache_status() ORDER BY bytes_resident DESC; -- 8. Audit where symbols come from (and whether any source egresses). SELECT * FROM symbols.main.sources(); ``` ### 函数 | 领域 | 签名 | 类型 | | --- | --- | --- | | 解析单个帧 | `symbolicate(build_id VARCHAR, address UBIGINT) → STRUCT(function, file, line, inlined LIST, module, debug_id, status)` | 标量 | | 仅函数名(快速路径) | `function_name(build_id, address) → VARCHAR` | 标量 | | 仅内联链 | `inline_frames(build_id, address) → LIST` | 标量 | | 名称还原 (Demangle) | `demangle(mangled VARCHAR [, lang]) → VARCHAR` | 标量 | | 解析列(逐行) | `resolve(build_id, address) → TABLE(...)` | 表输入输出 † | | 一次性解析列表 | `resolve_batch(LIST) → TABLE(frame_idx, ...)` | 表输入输出 † | | 检查调试文件 | `module_info(blob BLOB)` / `module_info(path VARCHAR) → TABLE(...)` | 表 | | 符号源 | `add_source(kind [, path=>, url=>, bucket=>, enabled=>, secret=>])`, `sources()`, `drop_source(id)` | 表 | | 缓存 | `cache_status() → TABLE(...)`, `cache_evict(debug_id := NULL) → BIGINT` | 表 | 解析后的行结构(`resolve` / `resolve_batch`)输出 **每(输入帧 × 内联层级)一行**,按照从最内层到最外层的顺序排列,物理帧位于最后: `build_id, address, inline_depth, is_inline, function, function_raw, file, line, column, module, debug_id, status`。没有匹配模块的帧会产生 **一** 行, `status='not_found'`,所有符号列均为 NULL — 解析过程 **绝不会丢失帧,也绝不会导致扫描报错**。 ### 地址模型(调用者契约) `address` 是 **模块相对虚拟地址**(位于模块映像内,在 ASLR 偏移 / 加载基址之前)。调用者 — 或者是拥有模块映射的上游 `vgi-minidump`/`vgi-pprof`/`vgi-perf` 帧提取器 — **首先减去模块基址**。`module_info` 会报告映像的首选基址以供参考。 ### 符号源(本地优先,远程可选开启) | 来源 | 配置 | 出站流量 | 说明 | | --- | --- | --- | --- | | `dir` | `path => '/srv/debug'` | 无 | 首次使用时按 debug-id 索引 | | `glob` | `path => '/builds/**/*.{debug,pdb,dSYM}'` | 无 | 递归;支持 `{a,b}` 花括号交替匹配 | | `debuginfod` / `s3` / `http` | `url=>` / `bucket=>`, `enabled => true`, `secret =>` | **网络** | 可选开启,**默认关闭**;通过 SDK 密钥提供程序传递凭据 | 默认姿态是 **气隙隔离安全** 的:在未启用任何远程源的情况下, 网络出站流量为零。(远程 *获取* 已注册并受出站流量限制,但在当前版本中不予执行 — 设计上本地优先;参见 [更新日志](CHANGELOG.md)。) ## 安全加固(不受信任的调试文件) 调试文件会受到攻击者的影响(崩溃语料库 / 恶意软件样本自带的符号)。格式错误的 ELF/PDB 会产生清晰的逐行错误,**绝不会**导致崩溃、 挂起或 OOM: - **逐行故障隔离** — 格式错误的模块会为需要它的帧产生 `status='error:'` (`kind ∈ {truncated, bad-magic, bad-build-id, corrupt-line-program, corrupt-inline-tree, unsupported-format, nesting-limit, alloc-cap}`);所有其他模块和帧均不受影响。 - **受限的分配与递归** — 对解析大小、内联树深度和 单元数量设有上限;恶意的二进制数据块无法引发 OOM 或栈溢出。 - **`catch_unwind` 遏制** — 底层的 DWARF/PDB 库在对抗性输入下可能会 `panic` (例如算术溢出);该 panic 会被捕获并 转换为清晰的错误行。 - **`cargo-fuzz` 零 panic 门禁** + 针对 ELF/Mach-O/ PE/PDB 和 build-id 路径的 proptest 无 panic 测试套件 (CI)。 - **负缓存** 可防止放大效应 — 缺失的 build-id 仅会被记录一次 并短路处理。 ## 构建与测试 ``` cargo test --workspace # unit + golden DWARF + cache + proptest no-panic cargo clippy --all-targets -- -D warnings cargo build --release # → target/release/symbols-worker ./run_tests.sh # haybarn SQLLogic e2e ``` 端到端测试套件需要 haybarn 工具(一次性): ``` uv tool install haybarn-unittest echo "INSTALL vgi FROM community;" | uvx haybarn-cli ``` 请参阅 [`CLAUDE.md`](CLAUDE.md) 了解架构、缓存内部原理以及如何 重新生成 Mach-O dSYM 黄金测试用例。 ## 许可证 Query Farm 源码可用 — 参见 [`LICENSE`](LICENSE)。版权所有 2026 Query Farm LLC — https://query.farm
标签:DuckDB, DWARF, Rust, 可视化界面, 多线程, 崩溃分析, 性能分析, 符号化, 网络流量审计, 通知系统