query · diff
git:20261025.c765cc3 to git:20261025.3bac09b
1 added, 5 removed. Audit A to A.
---
name: query
description: Choose the rllvm-query tool that answers a question about a captured program, and report its answer without over-claiming. Use for questions about definitions, callers, callees, reachability, function pointers or external calls in captured bitcode, and every time an rllvm-query answer is about to be reported.
---
# Querying captured bitcode
Load the program first with `load_catalog` or `inventory` (the `capture`
skill). With more than one catalog loaded, pass `catalog`.
Without the MCP server, the same queries run as
`rllvm-query --catalog catalog.json <query>`, with kebab-case names
(`indirect-targets`). Output is text with its caveats in a footer; `--json`
prints the envelope described below, and `--full` adds scope, analysis and
uncertainty to the text.
Each invocation reloads the whole catalog, which on a large program takes
seconds. For many queries, pipe them on stdin to `rllvm-query --catalog
catalog.json` with no query argument, one per line as written on the command
line: the catalog loads once, each text answer follows a `== <query>` line,
and `--json` prints one envelope per line. Every line is checked before the
catalog is read.
Loads also reuse each module's extracted facts from a persistent cache keyed
by its content hash, so a rerun over the same bitcode skips re-extraction.
`analysis.cache` (and `--full`) reports hits, misses and disk use; a cached
answer is identical to an uncached one. `rllvm-query cache` reports the
cache's location and per-generation disk use, and `rllvm-query cache clear
[--stale]` prunes it — `--stale` keeps this binary's own generation and
removes only ones an older or newer LLVM left behind.
When a warning about the facts cache appears, offer `rllvm-query cache clear
--stale` or `cache clear` and run neither without the user's approval.
## Choosing the query
| Question | Tool |
| --- | --- |
| Where is X defined, and in which configurations? | `defs` |
| What runs at `file:line`? | `at` |
| Who calls X? What does X call? | `callers`, `callees` |
| Where is X's address taken? | `uses` |
| Who can call X through a pointer? | `uses` for address-taken sites, then `indirect_targets` at each |
| What can this indirect call reach? | `indirect_targets` at the call site (`heuristics: true` adds the address-taken inventory) |
| Which functions could an unresolved indirect call reach, grouped by the record field it dispatches through or the slot it reads its callee from? | `resolution_candidates` (`resolution-candidates`): candidates, never edges; no walk follows them |
| What comparisons, clamps, masks or moduli constrain a value, such as a length handed to a copy? | `guards` (`--at file:line` with `--operand N`, counting from 0, or `--return`; or `--function F --local NAME`, adding `--at file:line` for a local reused for several values, which reads it at that use (a read of the value it holds there, possibly through another variable that holds the same value inside the local's scope); a name several variables share, such as a shadowed local, is refused with the line each is declared at, though a variable of an inlined callee does not count; `--from F` bounds a parameter's callers): evidence only, so an empty list is not "unbounded"; reads the anchor's module through `opt` from `llvm_bindir`, so over MCP it needs `load_catalog`, not `inventory` |
| Can A reach B — for example, is a vulnerable function reachable? | `reach`; if it finds no path, `closure` to see where the search stopped |
| Every function on some path from A to B, and the edges among them | `slice`; `emit_module` (`--emit-module OUT.bc`) also writes their definitions as one module |
| Everything that reaches X, or that X reaches | `closure` with `direction` `in` or `out` |
| What does the program call outside itself? | `externals` |
| Which Rust functions can C call — the FFI surface? | `ffi_exports` |
A name can be the mangled symbol, the full demangled reading, or a bare
identifier. A bare identifier matches a C++ demangled reading that contains
it as a whole identifier; a C symbol has no reading, so a bare C name
resolves only to its own exact symbol. When a bare name matches nothing,
find the exact symbol with `at`, `callers` or `callees`, then query that.
The file in a `file:line` matches as the debug info recorded it, or as any
path that names the same file from the working directory: relative,
absolute, or through a symlink.
## Recording resolved indirect calls
To close gaps in a call graph, use the `complete-callgraph` skill; it runs
the loop below.
When you resolve an unresolved indirect call by reading the code, record it
in the call-graph overlay. The overlay is a file beside the catalog,
`<catalog stem>.overlay.jsonl` unless another is named; the catalog is never
changed. Records are the same over MCP and the command line:
- `{"op":"add","via_field":{"record":"ops","offset":8},"to":"h3","confidence":"high","provenance":["init: o->handler = h3"]}`
attaches to every unresolved site through `ops@8`; `"via_slot": <a
group's slot>` instead covers every unresolved site reading that slot,
and `"site": <call site id>` names one. `to` is an exact symbol naming one definition
(ODR copies count as one), or `{"module_id", "symbol"}`. `provenance` is
required.
- `{"op":"verify","edge":<key>,"tool":...,"verdict":"confirmed|refuted|inconclusive"}`
and `{"op":"retract","edge":<key>,"reason":...}` name an edge's key.
- `{"op":"annotate","anchor":{"kind":"argument","site":"clamp.c:14","operand":2},"verdict":"infeasible","confidence":"high","provenance":["clamp.c:13: n = len < CAP ? len : CAP"]}`
is your own feasibility verdict on a value, never proof. `anchor` is an
`argument` (`site`, `operand`, counting from 0), a `return` (`site`) or a
`local` (`function`, `name`), which carries no use-site, so a local reused
for several values cannot be annotated; `site` is `file:line` or a call
site id;
optional `from` names one function. `verdict` is `feasible`, `infeasible`
or `conditional`. It is checked against the catalog on write, reading the
anchor's module (over MCP, a program loaded with `load_catalog`): an anchor
that does not resolve, or is ambiguous, is rejected. rllvm-query never
writes one itself.
- `{"op":"verify","annotation":{"anchor":<anchor>,"from":...},"tool":...,"verdict":"confirmed|refuted|inconclusive"}`
and `{"op":"retract","annotation":{"anchor":<anchor>,"from":...},"reason":...}`
name an annotation by its anchor and `from` (omitted when it has none).
- A batch applies all or none; an error names the bad record. An edge on a
bounded or direct site is rejected.
- After a rebuild the overlay refuses to load, naming both fingerprints. It
also refuses one written by an rllvm-query that numbers call sites
differently. To start again, move or delete the file, or name another
with `--overlay` (MCP: `path`).
- A save or compaction refuses when another writer changed the file since
it was opened; reopen it and record again.
Over MCP:
- `record_edges` takes them as `records`, attaching the overlay beside the
catalog first if none is loaded. They are walked at once but stay in
memory until `save_overlay`, the only tool that writes: it appends them
and reports how many were `saved`.
- `load_overlay` attaches the default file, or `path` — required for a
program loaded with `inventory`. It refuses to replace an overlay holding
unsaved records unless `discard_pending` is true.
- `list_overlay` shows the edges, the sites they cover, the `annotations`
with their `verdict`, and `pending` unsaved records.
- Unloading a catalog drops its overlay and reports how many unsaved records
went with it; reloading a rebuilt catalog drops an overlay bound to the
old build. Save before unloading, reloading, or ending the session;
unsaved records are not written on exit.
On the command line, pipe them as JSON lines to `rllvm-query --catalog
catalog.json overlay record`, which saves at once; `--overlay PATH` names
another file. `overlay list` shows edges and the sites they cover, then
annotations with their verdict; `overlay compact` rewrites the file as the
current edges and annotations.
Overlay edges are hypotheses, never proof: no answer uses them unless asked.
`reach`, `closure` and `slice` walk them with `include_overlay`
(`--include-overlay`), skipping refuted edges and any below `min_confidence`
(`--min-confidence low|medium|high`, default `low`). A file `--overlay` names
must exist for a walk to read it. Over MCP they need an overlay that
`load_overlay` or `record_edges` attached; without one, `include_overlay` is
an error, never a direct-only answer.
- Every step through one is `kind: agent` with its `confidence`,
`provenance` and `verdict`; quote the provenance when reporting it.
`uncertainty.agent_path_steps` counts them. Non-zero means the
path is not proven: report it as a hypothesis, never as reachability.
- `closure` lists functions reached only through agent edges in
`uncertainty.agent_reached`; `uncertainty.overlay` gives `covered_sites` of
`unresolved_sites`.
- A `slice` counts its agent edges the same way. To check one edge in scope,
`emit_module` writes the slice's definitions, cut out with the
`llvm-extract` and `llvm-nm` in the configured `llvm_bindir` and joined
with the configured `llvm-link`, as one small `.bc` to verify or re-read;
the answer gains `emitted`. An alias comes with the function it stands
for. It needs a program loaded with `load_catalog`, writes nothing for an
empty slice, and refuses when a `static` function or variable that stays
in one module's part shares its name with any function or variable
another part holds, since cutting it out makes it external.
Compiler-private data such as string literals is not checked: same-named
copies from two parts merge into one declaration.
Annotations are a separate opt-in: `reach` and `slice` take
`include_annotations` (`--annotations`, default off, independent of
`include_overlay`). It surfaces, under `uncertainty.annotated_flows`, each
`infeasible` or `conditional` annotation, not refuted, whose anchor lies on
the returned path, labeled `agent`. It is a hypothesis, never proof of
unreachability, and the path is never removed. An annotation recorded with a
`from` attaches only when the walk's `from` names that same single function;
a `from` matching several functions shows none of them. The JSON omits
`annotated_flows` when none attach. It is an error with no overlay attached
(over MCP, none loaded: call `load_overlay` or `record_edges`) or an overlay
holding no annotations.
## Is a vulnerable function present and reachable?
Ask per build: features and configurations change what is compiled in.
1. `defs` on the function. No result: it is not in this captured program.
2. `callers`. None: it is linked but has no captured direct caller; check
`uses` for its address being taken before concluding anything.
3. `reach` from each entry point to the function: `main`, or the exported
API — `ffi_exports` lists a Rust library's. A path is evidence; no path is
not proof (see below).
4. `callees` on the entry point shows what it actually does on the way.
## Reading the answer
Report what the answer supports, and say what it does not:
- **`scope` is not coverage.** `scope` is what the catalog claims; `analysis`
is what actually parsed. Report failed, missing, unsupported or unbuilt
modules; an answer covers only what was analyzed.
- **No path is not unreachable.** An empty `reach` means no path over
*resolved* edges. Mention `uncertainty.indirect_call_sites` and the
frontier; say "no path found", never "unreachable".
- **`!callees` is an upper bound.** `llvm_target_bound` says an indirect call
cannot target anything outside the set — not that it calls every member.
- **Address-taken lists are heuristics.** They appear in `indirect_targets`
only when called with `heuristics: true`, and are never call edges.
- **Say how the name matched.** `resolution[].matched` is `mangled`,
`demangled` or `fuzzy`; absent means the name matched nothing, which you
report as no match, not silence. A fuzzy match is a guess to confirm with
the user.
- **Know what a source digest proves.** `status_basis` is what `source_status`
was checked against: `compiler` or `capture` dates from the build,
`inventory` only from when the catalog was written. `source_status`:
`modified` means locations may be off, `missing` means the file is gone,
`unknown` means no digest was recorded to check.
- **Code without bitcode is invisible.** Assembly, prebuilt libraries and the
Rust standard library contribute no functions; calls into them appear in
`externals` as unbound. `indirect_targets`' `assumptions` state that
`dlopen` and callbacks registered by uncaptured code escape its bound.
- **An FFI surface is only as complete as its attribution.** `ffi_exports`
lists Rust definitions exported under an unmangled name, each attributed
to Rust by debug info or by the module's producer.
`uncertainty.functions_of_unknown_language` counts unmangled definitions
it could not attribute and did not search — non-zero for code built
without `-g`, or compiler-generated code such as a Rust binary's C
`main`. Report it; a per-object catalog avoids it.
- **A field is evidence, not a target.** An indirect site's `via_field` and a
use's `field` name the record field (`ops@8`: record and byte offset) the
pointer is loaded from or stored into. No field means the IR proved none,
not that there is none. `basis` says which IR evidence spoke; the member
`name`, from debug info, is display only.
- **So is a slot.** An indirect site's `via_slot` names the one location its
callee is read from: a `global`, a function's `param`, or the location an
accessor `returned` the address of. A slot group's candidates are the
functions found flowing into it through stores, arguments and accessors in
the captured program; a path the facts do not follow, such as `memcpy` or
pointer arithmetic, can add more.
- **A guard list covers one function.** `guards` traces the value inside the
anchor's function only. A guard's `depth` is 0 on the value itself, or on
a value it only widens (the bound holds across zext or sext when the
guard's signedness matches the cast's), and N on a value N steps up the
chain it is computed from, a truncation counting as a step; that is not by
itself a bound on the value. Its `affine` gives the anchored value as
`scale * guarded + offset` when constant arithmetic links them. Both
coefficients are decimal strings of integers that may exceed 64 bits:
parse them as big integers, never as JSON numbers. `verified` means the
bound soundly carries to the anchored value. Report a guard with
`verified: false` as a lead, never a bound, with its `caveat`: the
relation is not affine, the guard bounds a value computed from the
anchored one (such as a clamp of it used later), the bound wraps when
carried to the anchored value, or it could not be mapped (no constant, or
no branch edge that decides which side of a comparison holds at the
- anchor). A clamp of the anchored value made of a comparison (a branch or
- select clamp, not an intrinsic umin or saturation) also lists that
- comparison, which bounds the anchored value itself: verified only when its
- branch's edge decides the anchor, a lead otherwise.
- `verified` does not mean the guard runs on every path: report a
+ anchor). `verified` does not mean the guard runs on every path: report a
bound as unconditional only when it is `verified` and its `relation` is
`dominates` or `defines`; a verified one that is on_path is conditional.
The numeric cap lives in `bound`. A verified guard with no `bound` is
capped by a runtime value, the guard's operand: a sound relation, not a
numeric limit, so never report a number for it; to reach one, anchor on
that operand and trace it. A load is followed to the nearest store before
it in its block, which overwrites any earlier one; a call, another store
or any other operation between them (an atomicrmw, a cmpxchg, a volatile
load), which is never skipped to reach an earlier store, or stores only in
other blocks make every guard reached through the load a lead, with that
caveat, and report the trace lost there. A `relation` of `dominates` runs
before the use on every path but does not say which way its branch goes;
one that is on_path lies on some path to the use, not every one.
`crossings` name the call where the value enters or leaves: ask `guards`
there next. `from` only keeps crossings from callers on a path from one
named function; nothing between it and the anchor was examined.
`uncertainty.value_trace_lost` means the list is partial. `guards`
promotes the anchor's module with `mem2reg`, so a local held in a register
— any promotable scalar — is traced whatever optimization level the
capture used, `-O0` included. A value `mem2reg` cannot promote, such as a
struct field or an address-taken local, stays a memory load: a lost trace
unless the load is neither volatile nor atomic and a store of its type,
also neither, to the same address runs before it. Addresses match only
syntactically (the same pointer, or a `getelementptr` alike), so a field
of a global never matches. Capture at `-O0` for a faithful call graph; it
does not stop `guards` from tracing promotable scalars.
- **ODR copies are one definition.** A template or `inline` body emitted into
many translation units is one function; plain `weak` copies may differ and
stay ambiguous.