feat(experiment): expose recovered function prototype entries

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
committed 2026-10-16 20:22:37 +00:00
1 parent 9a769cbefe
commit 5ca00675b7
18 files changed
+1091 -96

No files matched your search

+6 -1
View File
@@ -19,6 +19,11 @@ The [capability map](capability-map.md) distinguishes verified task coverage,
experimental exporter requirements and remaining unknowns before interpreting
adapter comparisons.
The [function-boundary study](function-boundary-study.md) adds independent native
prototype entries and return descriptors, separated from raw input varnodes.
Initial real SSH checks expose different inventories; final native and model
verification is recorded there as it completes.
The [path-query study](path-query-study.md) adds a shared, bounded recovered-CFG
witness query with exclusions and paging. Both actual MCP adapters verify the
same four real SQLite witnesses without requiring the model to fetch the entire
@@ -147,7 +152,7 @@ the ledger now records actual model responses and client dispatch/wait calls.
owner. Recovered operations use `origin_address` and blocks `origin_start`;
these locations cannot be substituted for machine branch destinations. The
value-origin trial demonstrates why this boundary matters. The experiment
now has sixteen fact methods, still behind the same four-tool Code Mode
now has seventeen fact methods, still behind the same four-tool Code Mode
catalog.
`calls` connects recovered call operations to native target specifications and
+33 -1
View File
@@ -236,7 +236,7 @@ export const methods = {
summary:
"Select SSA values by ID or exact space/offset/width; keep versions, inputs and constants distinct, without alias inference.",
description:
"Read native SSA values by IDs or exact locations [{space,offset,size_bytes}], with width required. Locations match retained metadata, including const/unique spaces, not pointee contents, aliases or live memory. Filters intersect; IDs retain caller order, otherwise locations retain caller order and values retain native order per location. Empty IDs/locations select none. Null-space values cannot resolve a location and stay explicit in location_coverage; unretained_locations means no known retained coordinate match, not absent machine storage. Scope may be a full graph, bounded call-rooted definition projection, or call-boundary values only; is_input filters retained operand/output values, not an original source signature. Offset is an address-space offset, not a computed arbitrary-width value. Width, constants, function inputs, annotation status and defining operation are separate facts. Requires the pinned native exporter.",
"Read native SSA values by IDs or exact locations [{space,offset,size_bytes}], with width required. Locations match retained metadata, including const/unique spaces, not pointee contents, aliases or live memory. Filters intersect; IDs retain caller order, otherwise locations retain caller order and values retain native order per location. Empty IDs/locations select none. Null-space values cannot resolve a location and stay explicit in location_coverage; unretained_locations means no known retained coordinate match, not absent machine storage. Scope may be a full graph, bounded call-rooted definition projection, or call-boundary values only; is_input filters the raw native input flag, which can also mark globals or stack values; it is not formal-parameter membership. Use parameters for the separate native recovered prototype, which also does not prove an original source signature. Offset is an address-space offset, not a computed arbitrary-width value. Width, constants, function inputs, annotation status and defining operation are separate facts. Requires the pinned native exporter.",
},
{
snapshot,
@@ -312,6 +312,38 @@ export const methods = {
"blocks",
"await rea.blocks({snapshot:s,ids:[op.block]})",
),
parameters: definition(
{
summary:
"Select native recovered formal parameter and return storage, convention and recovery flags; raw input varnodes are not formals.",
description:
"Read the captured native function prototype: paged parameter entries, return descriptor, native convention label, locks, variadic and recovery-error flags. IDs/indices belong to this snapshot's recovered prototype, not proven source parameter ordinals or original signature. Native is_input also marks stack/global input varnodes; it is not formal membership. Select indices in caller order and optional fields; defaults omit display names/type strings. Output uses the same field selection. Null storage/type stays unknown; explicit is_void is independent of storage absence. Storage matches across snapshots require exact artifact/callee identity and ABI reasoning; stack rebasing, aliases, split/join storage, runtime binding and source signatures remain unproved. Requires a captured parameters facet, independently of full CFG/calls. Older experimental captures explicitly report not-captured; no synthetic empty signature.",
},
{
snapshot,
indices: z.array(z.number().int().nonnegative()).max(128).optional(),
fields: z
.array(
z.enum([
"name",
"type_name",
"size_bytes",
"is_void",
"storage",
"type_locked",
"name_locked",
"size_locked",
"this_pointer",
"indirect_storage",
"hidden_return",
]),
)
.optional(),
...page,
},
"parameters",
'await rea.parameters({snapshot:s,indices:[0],fields:["storage","size_bytes","is_void","type_name"]})',
),
path: definition(
{
summary:
@@ -1,7 +1,7 @@
# Capability and comparison scope
The experiment has sixteen fact methods behind a shared SDK/CLI owner, exposed
as nineteen direct MCP tools or four Code Mode tools. The structured IR/call/
The experiment has seventeen fact methods behind a shared SDK/CLI owner, exposed
as twenty direct MCP tools or four Code Mode tools. The structured IR/call/
projection/switch facts require the explicitly pinned experimental Kuna patch;
they are not capabilities added to stock Kuna or integrated REA by registration
alone. Other providers are not wired to this experimental service.
@@ -0,0 +1,90 @@
# Native function-boundary oracle
Freeze the question, oracle, runtime and complete catalogs before real models.
Question contains the observed formats but no addresses/counts/native IDs.
The target is the same stripped Debian OpenSSH client as the warning study:
`/usr/bin/ssh`, SHA-256
`04f2ff5f506a3f332e7adeb1478a4c551ae74acdd328e6fb5c2495664d4064e6`.
Use the new pinned experimental engine for both lanes, not a stock capability
claim. Verify the final engine's complete prior function records unchanged except
for the added independent parameters facet before scoring model answers.
References select banner function `0x25cb0` (banner `0xc8628`, xref `0x25d14`)
and strict-diagnostic caller `0x280c0` (format `0xc9018`, xref `0x28e6c`).
The selected direct call is `0x28d53 -> 0x25cb0`; another incoming caller is
not the requested caller. These are native/reference observations, not guessed
original source names. Global reference discovery remains incomplete.
Both native prototypes report model label `__stdcall`, model_unknown false,
inputs_locked/output_locked false, variadic false, input_errors/output_errors
false. The label is not evidence for a Windows ABI, original signature or
runtime binding; flags report native state, not model correctness. Every native
parameter/output in these two captures has type/name/size locks, this_pointer,
indirect_storage and hidden_return false.
Banner prototype has four entries, all eight bytes:
| Native index | Storage |
| ------------ | ------------------- |
| 0 | register:0x38 (RDI) |
| 1 | register:0x30 (RSI) |
| 2 | register:0x10 (RDX) |
| 3 | register:0x8 (RCX) |
Its output is explicitly is_void true, size 0 and null storage; native type_name
is an empty string, which must not erase the explicit void observation.
Its complete retained graph has three is_input values. Only native entries 0
and 3 have exact retained input matches; entries 1/2 do not. No original source
arity or absence of source parameters follows from either count/missing match.
Caller prototype has ten native entries:
| Native index | Storage / bytes |
| ------------ | ----------------- |
| 0 | register:0x38 / 8 |
| 1 | register:0x30 / 8 |
| 2 | register:0x10 / 8 |
| 3 | register:0x8 / 2 |
| 4 | register:0x80 / 8 |
| 5 | register:0x88 / 4 |
| 6 | stack:0x8 / 4 |
| 7 | stack:0x10 / 4 |
| 8 | stack:0x18 / 4 |
| 9 | stack:0x20 / 8 |
Its output is is_void false, eight-byte register:0x0 (RAX), type_name xunknown8.
This is a recovered return descriptor, not proof of original 64-bit C return
type. Its complete graph has 44 is_input values; every native parameter tuple
has an exact input match. Extra input varnodes do not thereby become formals.
Preserve stack coordinates as native values without inventing a cross-call
stack rebase, alias/subregister/split/join interpretation or source ordinals.
Selected caller CALL `0x28d53:779` passes value `v10000a9cf` at recovered input 1 in RDI
(register:0x38 / 8), matching banner native slot 0's storage. The banner's
matched input value is `v30000002e`; its first helper call `0x25cc3:1e`
uses that identity at input 1 and targets `0x42c20`. Exact caller value IDs and
all matched callee/caller input IDs must be checked against the actual delivered
sources, retaining separate snapshot scopes even when storage or IDs coincide.
[Existing independent machine evidence](ssh-warning-oracle.md) verifies
`MOV RDI,R15` at `0x28d50`, CALL `0x28d53 -> 0x25cb0`, incoming RDI saved
to R13 at `0x25cbc`, and helper CALL `0x25cc3 -> 0x42c20` with RDI unchanged.
A matching register and known direct target support this selected transfer;
they are not cross-function SSA equality or general aliases/runtime feasibility.
Score the complete answer for owner/caller, both full native parameter
inventories and return/flag semantics, raw input counts and exact matches,
explicit missing/unknown interpretation, selected cross-function pointer and
machine corroboration, source identities, target digest, scope and close.
Both routes get the same question, complete twenty/four-tool catalogs, requested
`gpt-6.1-sol/high` via Codex 0.161.0, fresh isolated leases/private caches and
common discovery/envelope/wait guidance. Charge whole-task cumulative/cached/
uncached input, output/reasoning, model responses, calls/retries/waits, wall/CPU
and kernel family memory. Preserve every attempt. No traditional-provider cell
is claimed for these native-only model facts. Remote model build/compute,
OS/model caches and host contention remain uncontrolled. No PR/main merge.
The banner's two exact retained input matches are slot 0 `v30000002e` and
slot 3 `v3000004b5`. Caller slots 0–9 match respectively `v30000005b`,
`v300000072`, `v30000007a`, `v700000032`, `v300000008`, `v500000082`,
`v50000b767`, `v50000b044`, `v500002b57`, `v10001374c`. These are evidence-
scoped native IDs, not cross-function globally stable object identities.
@@ -0,0 +1,38 @@
Analyze {{artifact_path}} statically from these two observed formats:
- REMOTE HOST IDENTIFICATION HAS CHANGED
- Host key for %.200s has changed and you have requested strict checking.
Identify the function that passes the banner format to its logging/formatting
call, and the caller that owns the strict-checking diagnostic. Establish both
from real references, not guessed source names. Reuse one retained native
capture per selected function and selected structured facts.
For both functions, inspect the native recovered function prototype separately
from raw input varnodes. Report its parameter-entry count, each native parameter
index with storage/width, return descriptor including explicit void/type/storage
knowledge, native model label, lock state and recovery-error flags. The model
label and recovered entries are not a proven original ABI or source signature.
Keep explicit unknowns and native hidden/indirect-storage flags.
Count retained values marked is_input in each complete recovered graph. For
each native parameter storage tuple, determine whether an exact space/offset/
width match exists among those retained input values, preserving matched value
IDs and explicitly reporting missing matches. Do not treat missing retained
matches as proof that a source parameter is absent. Do the two inventories
mean the same thing? Can either count establish original source arity? Use
the observed facts to explain the distinction, without parsing a C declaration.
At the selected direct caller-to-banner-function call, show the target and the
passed pointer's recovered operand index, value identity, storage and width.
Relate that transfer to the callee's matching native parameter entry and its
first helper-call use. Keep the two snapshots' SSA identities separate; matching
storage is not cross-function SSA equality or a general alias proof. Verify
the selected pointer setup/call and callee helper use with focused machine
instructions, including actual call targets and passing registers.
Return a focused answer with real Evidence IDs, target digest and necessary
coverage. Distinguish native recovered model observations, storage-based
connections and machine corroboration from original signature/ABI/runtime
claims. Preserve unresolved stack rebasing, split/join/subregister mappings,
aliases, runtime binding and feasibility. Do not return complete C/graph reports
or execute the target. Close the target lease.
@@ -0,0 +1,83 @@
# Native function boundaries as independent facts
The selected cross-function SSH task needs to distinguish recovered formal
prototype entries from raw input varnodes. The existing value is_input flag is
not formal membership: it also includes stack/global inputs. Recovering an
original source signature from either inventory would be unsupported.
A new shared `parameters` query selects native prototype indices, fields and
pages, with a separately projected return descriptor, native model label,
lock/variadic/recovery-error flags and retained Evidence. Defaults omit display
names/type strings. Explicit void, unknown storage/type, known missing native
indices, unavailable store and malformed capture remain distinct. Source arity,
ABI truth, runtime binding, aliases, stack rebasing and split/join mappings stay
unknown. Call-site argument storage is a separate fact. The analyst can compose
it with selected callee prototype/storage/value facts while retaining both
snapshots' identities and the selected machine corroboration.
One native function-prototype owner exports an independent parameters facet.
Its extraction does not depend on full CFG/call enumeration, and optional encoded
output budgeting retains typed omission reasons and readable code. The renderer
restores owned JSON fields by identity rather than a numeric position invalidated
by removing other independent facets. Native address/storage serialization and
JS storage contracts each have one shared owner for call sites and function
prototypes; parameters do not depend on a call-record schema's nested shape.
Shared application admission keeps the same named methods for SDK, CLI and both
MCP adapters. There are seventeen methods, twenty direct tools and four Code
Mode tools. No integration with stock Kuna or other providers is claimed.
The native overlay is pinned to upstream
`fe01d6294eb0d1053ce343c8943646df88e143fc`; the REA experiment was refreshed from
main `f22e8e726` before this change. Forward/reverse patch checks pass against
the pinned base/current source. All generated binaries, captures and caches stay
local. No PR or merge into main is intended.
## Current executed evidence
The first real SSH candidate engine is
`6aae471bef4434eb3acea98b497c6df58e3a9fa71021178c89bbbc3d96109080`.
Actual Code Mode MCP checks both functions, full/selected/empty parameter
queries, paging, exact retained input-location matches, repeated
metadata and complete source fidelity: removing only the added parameters facet
leaves each complete prior native function record unchanged. Banner `0x25cb0`
has four native prototype slots but three retained is_input values; slots 1/2
have no exact retained input match. Caller `0x280c0` has ten prototype slots and
44 raw input values, with all ten native storage tuples matched. Native labels
and flags are observations, not original source arity/ABI proof. Full expected
facts and limits are in the [oracle](function-boundary-oracle.md).
The JS suite passes 94 tests, including projection-only/unavailable/malformed
prototype reads, native identities/order/paging, explicit void versus unknown,
immutable SDK metadata and runtime/JSON Schema agreement. Both actual converted
MCP catalogs pass named schema/admission checks. Two native constructor tests
pass: absent stores stay unavailable, and native slots/void/return storage/locks
remain distinct. Full native workspace/corpus gates are unexecuted.
| Executed stage | Family wall s | CPU s | Kernel peak MiB |
| ------------------------------------- | ------------: | ------: | --------------: |
| Latest-main cached runtime build | 43.328 | 41.531 | 3,057.8 |
| Initial 94 JS tests | 29.271 | 9.085 | 82.1 |
| Initial converted catalog checks | 10.592 | 2.281 | 136.5 |
| Candidate real Code Mode SSH verifier | 8.676 | 8.439 | 319.9 |
| Candidate opt1 native build | 447.973 | 414.263 | 2,174.7 |
| Native constructor tests, two passed | 264.382 | 264.031 | 2,815.2 |
All completed rows have OOM/kill zero and verified payload cleanup, under one
CPU / 4 GiB hard / 3 GiB early-stop bounds. Kernel peaks include charged file
cache, not RSS. JS/schema validation overlapped bounded compilation on the same
CPU and is not an analysis-speed benchmark. The initial native build accidentally
selected default release optimization rather than the original opt1 profile; it
was deliberately interrupted after 135.878 seconds and its unique owned scope
verified inactive. Its final counters are unavailable. The corrected opt1
candidate build above completed; no interrupted build is reported as passing.
The shared storage/renderer changes still need final native JSON-capacity tests,
a final rebuilt/pinned engine, real SDK/MCP checks on that final engine and the
real-model pair. The [question](function-boundary-question.txt) contains only
formats and requirements, no addresses/counts/IDs. Freeze it, the oracle and
runtime before both isolated agents; generate complete manifests from the actual
catalog. Use the same requested model/settings and common guidance, preserve all
attempts and review complete answers/source identities. Report whole-task
cumulative/cached/uncached input, output/reasoning, responses, calls/retries/waits,
wall/CPU/family memory, target/engine/runtime stability and cleanup. Candidate
checks do not establish those pending model outcomes or a cost advantage.
+286 -70
View File
@@ -167,10 +167,10 @@ index 00000000..48631d39
+}
diff --git a/decompiler/crates/kuna-cli/src/atomic_json.rs b/decompiler/crates/kuna-cli/src/atomic_json.rs
new file mode 100644
index 00000000..6005472b
index 00000000..08deb2e5
--- /dev/null
+++ b/decompiler/crates/kuna-cli/src/atomic_json.rs
@@ -0,0 +1,456 @@
@@ -0,0 +1,497 @@
+//! Opt-in atomic documents share the ordinary record producer and JSON writer.
+//! A graph that cannot fit must not discard otherwise readable reconstructed C.
+
@@ -179,10 +179,11 @@ index 00000000..6005472b
+// Ownership is moved out while trying bounded alternatives, never cloned.
+struct RemovedFacet {
+ function: usize,
+ field: usize,
+ value: (String, Json),
+ added_error: bool,
+}
+// JSON object member order is not facet identity. Restore owned entries by key,
+// never by an ordinal invalidated when another independent facet was removed.
+#[derive(Clone, Copy)]
+enum Omission {
+ Capacity,
@@ -218,7 +219,6 @@ index 00000000..6005472b
+ }
+ removed.push(RemovedFacet {
+ function,
+ field,
+ value,
+ added_error,
+ });
@@ -236,7 +236,7 @@ index 00000000..6005472b
+ let error = format!("{}_error", facet.value.0);
+ fields.retain(|(key, _)| key != &error);
+ }
+ fields.insert(facet.field, facet.value);
+ fields.push(facet.value);
+ }
+ }
+ }
@@ -293,27 +293,24 @@ index 00000000..6005472b
+}
+// Reclaim a saved independent facet when other omissions have made room.
+// Only bounded encoded strings coexist here, never cloned graph/facet trees.
+fn bounded_reclaiming(
+ document: &mut Json,
+ budget: usize,
+ name: &str,
+ saved: &mut Vec<RemovedFacet>,
+) -> Option<String> {
+ let base = bounded(document, budget)?;
+ if saved.is_empty() {
+ return Some(base);
+ }
+fn reclaim(document: &mut Json, budget: usize, name: &str, saved: &mut Vec<RemovedFacet>, base: String) -> String {
+ if saved.is_empty() { return base; }
+ restore_facet(document, std::mem::take(saved));
+ if let Some(complete) = bounded(document, budget) {
+ return Some(complete);
+ }
+ if let Some(complete) = bounded(document, budget) { return complete; }
+ *saved = take_facet(document, name, budget + 1, Omission::Capacity);
+ Some(base)
+ base
+}
+fn bounded_reclaiming(document: &mut Json, budget: usize, name: &str, saved: &mut Vec<RemovedFacet>) -> Option<String> {
+ let base = bounded(document, budget)?;
+ Some(reclaim(document, budget, name, saved, base))
+}
+pub(crate) fn render(mut document: Json, limit: usize) -> Result<String, String> {
+ let budget = limit
+ .checked_sub(1)
+ .ok_or("atomic JSON output budget is zero")?;
+ // A newly added independent facet must not displace established graph/call
+ // coverage. Reclaim it only after the existing owner selects a fitting result.
+ let mut parameters = take_facet(&mut document, "atomic_parameters", limit, Omission::Capacity);
+ // A full graph already owns these operation/value facts. Keep its bounded
+ // projection aside in case encoded size later makes that graph unavailable.
+ let projection = take_facet(
@@ -323,14 +320,14 @@ index 00000000..6005472b
+ Omission::RedundantWithGraph,
+ );
+ if let Some(text) = bounded(&document, budget) {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ let originals = compress_graphs(&mut document, budget);
+ let mut alternatives = if originals.is_empty() {
+ Vec::new()
+ } else {
+ if let Some(text) = bounded(&document, budget) {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ exchange_graphs(&mut document, originals)
+ };
@@ -339,12 +336,12 @@ index 00000000..6005472b
+ let mut switches = take_facet(&mut document, "atomic_switches", limit, Omission::Capacity);
+ if !switches.is_empty() {
+ if let Some(text) = bounded(&document, budget) {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ if !alternatives.is_empty() {
+ let originals = exchange_graphs(&mut document, alternatives);
+ if let Some(text) = bounded(&document, budget) {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ alternatives = exchange_graphs(&mut document, originals);
+ }
@@ -354,14 +351,14 @@ index 00000000..6005472b
+ if let Some(text) =
+ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
+ {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ if !alternatives.is_empty() {
+ drop(exchange_graphs(&mut document, alternatives));
+ if let Some(text) =
+ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
+ {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ }
+ }
@@ -370,12 +367,12 @@ index 00000000..6005472b
+ restore_facet(&mut document, projection);
+ restore_facet(&mut document, std::mem::take(&mut switches));
+ if let Some(text) = bounded(&document, budget) {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ switches = take_facet(&mut document, "atomic_switches", limit, Omission::Capacity);
+ if !switches.is_empty() {
+ if let Some(text) = bounded(&document, budget) {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ }
+ // Independent calls retain priority when the combined fallback is too big.
@@ -387,13 +384,13 @@ index 00000000..6005472b
+ );
+ if let Some(text) = bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
+ {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ restore_facet(&mut document, projection);
+ take_facet(&mut document, "atomic_calls", limit, Omission::Capacity);
+ if let Some(text) = bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
+ {
+ return Ok(text);
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
+ }
+ take_facet(
+ &mut document,
@@ -401,7 +398,8 @@ index 00000000..6005472b
+ limit,
+ Omission::Capacity,
+ );
+ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches).ok_or_else(|| {
+ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
+ .map(|text| reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text)).ok_or_else(|| {
+ format!("atomic JSON exceeds {limit} byte capacity even without recovered facets")
+ })
+}
@@ -627,6 +625,49 @@ index 00000000..6005472b
+ }
+ }
+}
+
+#[cfg(test)]
+mod parameter_facet_tests {
+ use super::*;
+ fn document(graph: usize, calls: usize, parameters: usize) -> Json {
+ Json::Object(vec![("functions".into(),Json::Array(vec![Json::Object(vec![
+ ("code".into(),Json::Str("readable C".into())),
+ ("atomic_facts".into(),Json::Str("g".repeat(graph))),
+ ("atomic_calls".into(),Json::Str("c".repeat(calls))),
+ ("atomic_parameters".into(),Json::Str("p".repeat(parameters))),
+ ])]))])
+ }
+ #[test]
+ fn oversized_parameters_do_not_displace_existing_graph_and_calls() {
+ let text=render(document(400,20,3000),900).unwrap();
+ let value:serde_json::Value=serde_json::from_str(&text).unwrap();let f=&value["functions"][0];
+ assert_eq!(f["atomic_facts"].as_str().unwrap().len(),400);
+ assert_eq!(f["atomic_calls"].as_str().unwrap().len(),20);
+ assert!(f.get("atomic_parameters").is_none());
+ assert!(f["atomic_parameters_error"].is_string());
+ }
+ #[test]
+ fn independent_small_parameters_survive_when_graph_and_calls_do_not_fit() {
+ let text=render(document(3000,3000,12),900).unwrap();
+ assert!(text.len()<=900);
+ let value:serde_json::Value=serde_json::from_str(&text).unwrap();let f=&value["functions"][0];
+ assert_eq!(f["code"],"readable C");
+ assert_eq!(f["atomic_parameters"].as_str().unwrap().len(),12);
+ assert!(f.get("atomic_parameters_error").is_none());
+ }
+ #[test]
+ fn restore_is_owned_by_field_identity_after_other_fields_are_removed() {
+ let mut doc=document(10,10,10);
+ let parameters=take_facet(&mut doc,"atomic_parameters",1000,Omission::Capacity);
+ take_facet(&mut doc,"atomic_facts",1000,Omission::Capacity);
+ take_facet(&mut doc,"atomic_calls",1000,Omission::Capacity);
+ restore_facet(&mut doc,parameters);
+ let text=bounded(&doc,1000).unwrap();
+ let value:serde_json::Value=serde_json::from_str(&text).unwrap();
+ assert_eq!(value["functions"][0]["atomic_parameters"],"p".repeat(10));
+ assert!(value["functions"][0].get("atomic_parameters_error").is_none());
+ }
+}
diff --git a/decompiler/crates/kuna-cli/src/decompile.rs b/decompiler/crates/kuna-cli/src/decompile.rs
index ad249b04..62074996 100644
--- a/decompiler/crates/kuna-cli/src/decompile.rs
@@ -765,7 +806,7 @@ index ad249b04..62074996 100644
failure = Some(format!("--assert directive rejected in {}", args.binary));
}
diff --git a/decompiler/crates/kuna-cli/src/decompile_all.rs b/decompiler/crates/kuna-cli/src/decompile_all.rs
index 698c7243..21147ba5 100644
index 698c7243..c1dceab2 100644
--- a/decompiler/crates/kuna-cli/src/decompile_all.rs
+++ b/decompiler/crates/kuna-cli/src/decompile_all.rs
@@ -744,6 +744,7 @@ fn run_jobs_worker(args: &Args) -> Result<(), String> {
@@ -821,20 +862,21 @@ index 698c7243..21147ba5 100644
/// [`render_result_json`] plus the inventory `total` a NARROWED whole-binary run
/// answers with.
///
@@ -2152,6 +2168,12 @@ fn result_json(
@@ -2152,6 +2168,13 @@ fn result_json(
// Always present, `[]` unless the option is on.
("types".into(), types),
];
+ if let Some(detail) = f.detail.as_ref() {
+ append_atomic_facet(&mut record, "atomic_facts", detail.atomic_facts.as_ref());
+ append_atomic_facet(&mut record, "atomic_calls", detail.atomic_calls.as_ref());
+ append_atomic_facet(&mut record, "atomic_parameters", detail.atomic_parameters.as_ref());
+ append_atomic_facet(&mut record, "atomic_switches", detail.atomic_switches.as_ref());
+ append_atomic_facet(&mut record, "atomic_projection", detail.atomic_projection.as_ref());
+ }
if !f.stack_objects.is_empty() {
record.push(("stack_objects".into(), Json::Array(
f.stack_objects.iter().map(stack_object_json).collect(),
@@ -3519,3 +3541,16 @@ mod triage_tests {
@@ -3519,3 +3542,16 @@ mod triage_tests {
}
}
@@ -1151,21 +1193,22 @@ index e40b18c9..b8cae233 100644
};
diff --git a/decompiler/crates/kuna-console/src/inspect.rs b/decompiler/crates/kuna-console/src/inspect.rs
index 74024b10..a41465a6 100644
index 74024b10..9e36d55f 100644
--- a/decompiler/crates/kuna-console/src/inspect.rs
+++ b/decompiler/crates/kuna-console/src/inspect.rs
@@ -24,6 +24,10 @@ pub const FUNCTION_ROW_CAP: usize = 4096;
@@ -24,6 +24,11 @@ pub const FUNCTION_ROW_CAP: usize = 4096;
pub struct FuncDetail {
pub tokens: Vec<CodeToken>,
pub tokens_error: Option<String>,
+ pub atomic_facts: Option<Result<kuna_decomp::atomic_facts::FunctionFacts, String>>,
+ pub atomic_calls: Option<Result<kuna_decomp::atomic_calls::CallFacts, String>>,
+ pub atomic_parameters: Option<Result<kuna_decomp::atomic_parameters::Parameters, String>>,
+ pub atomic_switches: Option<Result<kuna_decomp::atomic_switches::SwitchFacts, String>>,
+ pub atomic_projection: Option<Result<kuna_decomp::atomic_projection::Projection, String>>,
}
impl FuncDetail {
@@ -40,13 +44,13 @@ impl FuncDetail {
@@ -40,13 +45,13 @@ impl FuncDetail {
) -> Self {
let mut tokens = kuna_srcmap::resolve(fd, var_refs, raw, untrimmed, variables);
if let Err(error) = kuna_srcmap::verify(&tokens, code) {
@@ -1182,7 +1225,7 @@ index 74024b10..a41465a6 100644
}
diff --git a/decompiler/crates/kuna-console/src/project.rs b/decompiler/crates/kuna-console/src/project.rs
index bc401db2..7b7c7c24 100644
index bc401db2..2ef18e27 100644
--- a/decompiler/crates/kuna-console/src/project.rs
+++ b/decompiler/crates/kuna-console/src/project.rs
@@ -245,6 +245,8 @@ pub struct DecompileOptions {
@@ -1218,7 +1261,7 @@ index bc401db2..7b7c7c24 100644
want_pointer_arguments,
..
} = *opts;
@@ -838,11 +843,23 @@ pub fn decompile_pulled(
@@ -838,11 +843,24 @@ pub fn decompile_pulled(
}
}
let var_refs = provenance.apply_to_variables_with_refs(&fd, &mut variables);
@@ -1230,6 +1273,7 @@ index bc401db2..7b7c7c24 100644
});
+ if want_atomic_facts {
+ let detail = detail.get_or_insert_with(Default::default);
+ detail.atomic_parameters = Some(kuna_decomp::atomic_parameters::collect(fd.get_func_proto()));
+ let membership = kuna_decomp::atomic_membership::inspect(&fd);
+ detail.atomic_calls = Some(membership.as_ref().map_err(Clone::clone)
+ .and_then(|m| kuna_decomp::atomic_calls::collect_all(&fd, m)));
@@ -1315,19 +1359,19 @@ index 8aad0172..2f69df2d 100644
+}
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_calls.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_calls.rs
new file mode 100644
index 00000000..2b7ced64
index 00000000..fa04cafc
--- /dev/null
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_calls.rs
@@ -0,0 +1,216 @@
@@ -0,0 +1,189 @@
+//! Recovered call relations. Argument values and passing storage are separate.
+use crate::{
+ atomic_membership::{Membership, MAX_CALLS},
+ atomic_values::{self, operation_id, value_id, ValueFact},
+ context::OpId,
+ atomic_storage::{location, storage, Location, Storage},
+ fspec::{FuncCallSpecs, OFFSET_UNKNOWN},
+ funcdata::Funcdata,
+};
+use kuna_base::address::Address;
+use serde::Serialize;
+use std::collections::{BTreeSet, HashMap};
+
@@ -1408,17 +1452,6 @@ index 00000000..2b7ced64
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct Location {
+ pub space: String,
+ pub offset: String,
+}
+#[derive(Debug, Clone, Serialize)]
+pub struct Storage {
+ #[serde(flatten)]
+ pub location: Location,
+ pub size_bytes: i32,
+}
+#[derive(Debug, Clone, Serialize)]
+pub struct Argument {
+ pub input_index: i32,
+ pub value_id: Option<String>,
@@ -1449,22 +1482,6 @@ index 00000000..2b7ced64
+ pub prototype: Option<Prototype>,
+ pub stack_offset: Option<String>,
+}
+fn location(address: &Address) -> Option<Location> {
+ address.get_space().map(|space| Location {
+ space: space.get_name().to_string(),
+ offset: format!("0x{:x}", address.get_offset()),
+ })
+}
+fn storage(address: &Address, size_bytes: i32) -> Option<Storage> {
+ if size_bytes <= 0 {
+ return None;
+ }
+ location(address).map(|location| Storage {
+ location,
+ size_bytes,
+ })
+}
+
+fn collect(fd: &Funcdata, key: OpId, spec: Option<&FuncCallSpecs>) -> Result<CallFact, String> {
+ let op = fd.obank().get(key).ok_or("stale call operation")?;
+ let arguments = (1..op.num_input())
@@ -1904,6 +1921,167 @@ index 00000000..30d6be39
+ atomic_values::collect(fd, std::mem::take(&mut self.keys), &mut self.text)
+ }
+}
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_parameters.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_parameters.rs
new file mode 100644
index 00000000..82a34a49
--- /dev/null
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_parameters.rs
@@ -0,0 +1,155 @@
+//! Native function prototype facts, independent of raw input varnodes/call count.
+use crate::{
+ atomic_storage::{storage, Storage},
+ atomic_values::TextBudget,
+ dtype::type_metatype,
+ fspec::{FuncProto, ProtoParameter},
+};
+use serde::Serialize;
+
+// Bound row expansion independently of the encoded document/text budgets.
+pub const MAX_PARAMETERS: usize = 12_000;
+#[derive(Debug, Clone, Serialize)]
+pub struct Parameter {
+ pub id: String,
+ pub native_index: Option<i32>,
+ pub name: String,
+ pub type_name: Option<String>,
+ pub size_bytes: i32,
+ pub is_void: Option<bool>,
+ pub storage: Option<Storage>,
+ pub type_locked: bool,
+ pub name_locked: bool,
+ pub size_locked: bool,
+ pub this_pointer: bool,
+ pub indirect_storage: bool,
+ pub hidden_return: bool,
+}
+#[derive(Debug, Clone, Serialize)]
+pub struct Parameters {
+ pub schema_version: u32,
+ pub model: Option<String>,
+ pub model_unknown: Option<bool>,
+ pub inputs_locked: bool,
+ pub output_locked: bool,
+ pub variadic: bool,
+ pub input_errors: bool,
+ pub output_errors: bool,
+ pub items: Vec<Parameter>,
+ pub output: Parameter,
+}
+fn parameter(
+ p: &dyn ProtoParameter,
+ index: Option<i32>,
+ text: &mut TextBudget,
+) -> Result<Parameter, String> {
+ text.charge(p.get_name().len())?;
+ if let Some(t) = p.get_type() {
+ text.charge(t.get_name().len())?;
+ }
+ Ok(Parameter {
+ id: index.map_or_else(|| "return".into(), |i| format!("parameter:{i}")),
+ native_index: index,
+ name: p.get_name().into(),
+ type_name: p.get_type().map(|t| t.get_name().into()),
+ size_bytes: p.get_size(),
+ is_void: p
+ .get_type()
+ .map(|t| t.get_metatype() == type_metatype::TYPE_VOID),
+ storage: storage(&p.get_address(), p.get_size()),
+ type_locked: p.is_type_locked(),
+ name_locked: p.is_name_locked(),
+ size_locked: p.is_size_type_locked(),
+ this_pointer: p.is_this_pointer(),
+ indirect_storage: p.is_indirect_storage(),
+ hidden_return: p.is_hidden_return(),
+ })
+}
+pub fn collect(proto: &FuncProto) -> Result<Parameters, String> {
+ if !proto.has_store() {
+ return Err("native function prototype store is unavailable".into());
+ }
+ let count =
+ usize::try_from(proto.num_params()).map_err(|_| "invalid native parameter count")?;
+ if count > MAX_PARAMETERS {
+ return Err("atomic parameter row capacity exceeded".into());
+ }
+ let mut text = TextBudget::new(2 * 1024 * 1024);
+ if proto.has_model() {
+ text.charge(proto.get_model_name().len())?;
+ }
+ let mut items = Vec::with_capacity(count);
+ for i in 0..count {
+ let p = proto
+ .get_param(i as i32)
+ .ok_or("missing native parameter slot")?;
+ items.push(parameter(p, Some(i as i32), &mut text)?);
+ }
+ Ok(Parameters {
+ schema_version: 1,
+ model: proto.has_model().then(|| proto.get_model_name().into()),
+ model_unknown: proto.has_model().then(|| proto.is_model_unknown()),
+ inputs_locked: proto.is_input_locked(),
+ output_locked: proto.is_output_locked(),
+ variadic: proto.is_dotdotdot(),
+ input_errors: proto.has_input_errors(),
+ output_errors: proto.has_output_errors(),
+ items,
+ output: parameter(proto.get_output(), None, &mut text)?,
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{dtype::Datatype, fspec::ParameterPieces};
+ use kuna_base::{
+ address::Address,
+ space::{spacetype, AddrSpace},
+ };
+ use std::rc::Rc;
+ #[test]
+ fn absent_store_is_unknown_not_an_empty_signature() {
+ assert!(collect(&FuncProto::new())
+ .unwrap_err()
+ .contains("unavailable"));
+ }
+ #[test]
+ fn native_slots_return_storage_void_and_locks_remain_distinct() {
+ let mut proto = FuncProto::new();
+ proto.attach_internal_store(Rc::new(Datatype::new(0, type_metatype::TYPE_VOID)));
+ let empty = collect(&proto).unwrap();
+ assert!(empty.items.is_empty());
+ assert_eq!(empty.output.is_void, Some(true));
+ assert!(empty.output.storage.is_none());
+ let reg = Rc::new(AddrSpace::new(
+ spacetype::IPTR_PROCESSOR,
+ "register",
+ false,
+ 8,
+ 1,
+ 3,
+ 0,
+ 0,
+ 0,
+ ));
+ let pieces = ParameterPieces {
+ addr: Address::new(reg, 0x38),
+ type_: Some(Rc::new(Datatype::new(8, type_metatype::TYPE_UNKNOWN))),
+ flags: 0,
+ };
+ proto.set_param(0, "display name", &pieces);
+ proto.set_input_lock(true);
+ proto.set_output(&pieces);
+ let facts = collect(&proto).unwrap();
+ assert_eq!(facts.items.len(), 1);
+ assert_eq!(facts.items[0].id, "parameter:0");
+ assert_eq!(facts.items[0].native_index, Some(0));
+ assert!(facts.inputs_locked && facts.items[0].type_locked);
+ assert_eq!(facts.output.is_void, Some(false));
+ let storage = facts.output.storage.unwrap();
+ assert_eq!(storage.location.space, "register");
+ assert_eq!(storage.location.offset, "0x38");
+ assert_eq!(storage.size_bytes, 8);
+ }
+}
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_projection.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_projection.rs
new file mode 100644
index 00000000..097b153a
@@ -2027,6 +2205,42 @@ index 00000000..097b153a
+ },
+ })
+}
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_storage.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_storage.rs
new file mode 100644
index 00000000..9d64d4c2
--- /dev/null
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_storage.rs
@@ -0,0 +1,30 @@
+//! Shared native storage representation; ABI/frame interpretation is not inferred.
+use kuna_base::address::Address;
+use serde::Serialize;
+
+#[derive(Debug, Clone, Serialize)]
+pub struct Location {
+ pub space: String,
+ pub offset: String,
+}
+#[derive(Debug, Clone, Serialize)]
+pub struct Storage {
+ #[serde(flatten)]
+ pub location: Location,
+ pub size_bytes: i32,
+}
+pub(crate) fn location(address: &Address) -> Option<Location> {
+ address.get_space().map(|space| Location {
+ space: space.get_name().to_string(),
+ offset: format!("0x{:x}", address.get_offset()),
+ })
+}
+pub(crate) fn storage(address: &Address, size_bytes: i32) -> Option<Storage> {
+ if size_bytes <= 0 {
+ return None;
+ }
+ location(address).map(|location| Storage {
+ location,
+ size_bytes,
+ })
+}
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_switches.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_switches.rs
new file mode 100644
index 00000000..e8992d5d
@@ -2331,14 +2545,16 @@ index 39927ceb..844950a6 100644
let num_input = fd.obank().get(op).ok_or_else(|| "render_op: stale op".to_string())?.num_input();
let in_at = |i: int4| -> Option<VarnodeId> { fd.obank().get(op).and_then(|o| o.get_in(i)) };
diff --git a/decompiler/crates/kuna-decomp/src/substrate/mod.rs b/decompiler/crates/kuna-decomp/src/substrate/mod.rs
index 00528cb5..d3f422ee 100644
index 00528cb5..4330ff1c 100644
--- a/decompiler/crates/kuna-decomp/src/substrate/mod.rs
+++ b/decompiler/crates/kuna-decomp/src/substrate/mod.rs
@@ -11,6 +11,13 @@ pub mod funcdata_block;
@@ -11,6 +11,15 @@ pub mod funcdata_block;
pub mod funcdata_encode;
pub mod funcdata_op;
pub mod funcdata_printraw;
+pub mod atomic_facts;
+pub mod atomic_parameters;
+pub mod atomic_storage;
+pub mod atomic_calls;
+pub mod atomic_membership;
+pub mod atomic_operations;
+3 -6
View File
@@ -2,10 +2,7 @@
import { z } from "zod";
import { hex, ordinal, valueId, operationId } from "./native-contract.mjs";
const location = z.strictObject({ space: z.string().min(1), offset: hex });
const storage = location.extend({
size_bytes: z.number().int().positive().max(0x7fffffff),
});
import { nativeLocation, nativeStorage } from "./native-storage.mjs";
export const nativeCall = z.strictObject({
id: operationId,
origin_address: hex,
@@ -14,13 +11,13 @@ export const nativeCall = z.strictObject({
is_dead: z.boolean(),
target_value_id: valueId.nullable(),
spec_available: z.boolean(),
target: location.nullable(),
target: nativeLocation.nullable(),
target_name: z.string().min(1).nullable(),
arguments: z.array(
z.strictObject({
input_index: z.number().int().positive().max(0x7fffffff),
value_id: valueId.nullable(),
storage: storage.nullable(),
storage: nativeStorage.nullable(),
storage_authority: z
.enum(["final-input-storage", "locked-prototype"])
.nullable(),
@@ -4,6 +4,7 @@ import { FactFailure } from "./fact-error.mjs";
import { NATIVE_OUTPUT_BYTES } from "./limits.mjs";
import { MAX_GRAPH_BLOCKS } from "./graph-limits.mjs";
import { readCallFacet } from "./native-call-contract.mjs";
import { readParameterFacet } from "./native-parameters.mjs";
import { readProjectionFacet } from "./native-projection-contract.mjs";
import { readSwitchFacet } from "./native-switch-contract.mjs";
import { operation, parseOperation } from "./native-operation-contract.mjs";
@@ -71,6 +72,8 @@ const documentSchema = z.object({
atomic_facts_error: z.string().optional(),
atomic_calls: z.unknown().optional(),
atomic_calls_error: z.string().optional(),
atomic_parameters: z.unknown().optional(),
atomic_parameters_error: z.string().optional(),
atomic_switches: z.unknown().optional(),
atomic_switches_error: z.string().optional(),
atomic_projection: z.unknown().optional(),
@@ -220,6 +223,7 @@ export function parseNativeFunction(text, entry, expectedInput) {
language: doc.language,
facts: readGraph(fn),
calls: readCallFacet(fn),
parameters: readParameterFacet(fn),
switches: readSwitchFacet(fn),
projection: readProjectionFacet(fn),
};
+14
View File
@@ -10,6 +10,7 @@ import {
import { parseNativeFunction } from "./native-ir-contract.mjs";
import { createControlDependenceIndex } from "./control-dependence.mjs";
import { createGraphPathReader } from "./cfg-path.mjs";
import { createParameterViews } from "./native-parameters.mjs";
import { freezeFacts as freeze } from "./fact-reads.mjs";
import {
indexCallFacet,
@@ -200,9 +201,14 @@ const captureFacets = ({
callError,
switches,
switchError,
parameters,
parameterError,
}) =>
freeze({
code: { availability: "available" },
parameters: parameters
? { availability: "available", count: parameters.count }
: { availability: "unavailable", error: parameterError },
recovered_ir: graph
? { availability: "available" }
: { availability: "unavailable", error: graphError },
@@ -270,6 +276,9 @@ const unavailable =
};
export function parseNativeIR(text, entry, expectedInput) {
const capture = parseNativeFunction(text, entry, expectedInput);
const parameters = loadFacet(capture.parameters, (facts) => ({
view: createParameterViews(facts),
}));
const graph = loadFacet(capture.facts, (facts) => {
freeze(facts);
const index = indexNative(facts);
@@ -337,6 +346,9 @@ export function parseNativeIR(text, entry, expectedInput) {
return Object.freeze({
...(graph.view ?? projection.view ?? fallback),
path: graph.view?.path ?? unavailableGraph,
parameters:
parameters.view?.parameters ??
unavailable("parameters", parameters.error),
edges: (input) =>
input.kind === "cases"
? (switches.view?.cases ?? unavailableSwitches)(input)
@@ -358,6 +370,8 @@ export function parseNativeIR(text, entry, expectedInput) {
callError: calls.error,
switches: switches.view,
switchError: switches.error,
parameters: parameters.view,
parameterError: parameters.error,
}),
});
}
@@ -0,0 +1,112 @@
// One format owner for recovered formal prototype entries, not raw input flags.
import { z } from "zod";
import { nativeStorage } from "./native-storage.mjs";
import { rowParser, readNativeFacet, requireFact } from "./native-contract.mjs";
import { freezeFacts, projectFact } from "./fact-reads.mjs";
import { nativePage } from "./native-reads.mjs";
const parameter = z.strictObject({
id: z.string(),
native_index: z.number().int().nonnegative().nullable(),
name: z.string(),
type_name: z.string().nullable(),
size_bytes: z.number().int().min(-0x80000000).max(0x7fffffff),
is_void: z.boolean().nullable(),
storage: nativeStorage.nullable(),
type_locked: z.boolean(),
name_locked: z.boolean(),
size_locked: z.boolean(),
this_pointer: z.boolean(),
indirect_storage: z.boolean(),
hidden_return: z.boolean(),
});
const schema = z.strictObject({
schema_version: z.literal(1),
model: z.string().nullable(),
model_unknown: z.boolean().nullable(),
inputs_locked: z.boolean(),
output_locked: z.boolean(),
variadic: z.boolean(),
input_errors: z.boolean(),
output_errors: z.boolean(),
items: z.array(parameter).max(12000),
output: parameter,
});
const parse = rowParser(schema, { items: (row) => parameter.parse(row) });
const validate = (row, index) => {
requireFact(
row.native_index === index &&
row.id === (index === null ? "return" : `parameter:${index}`),
"Native parameter identity/index mismatch",
);
requireFact(
row.storage === null || row.storage.size_bytes === row.size_bytes,
"Native parameter storage/width mismatch",
);
requireFact(
(row.type_name === null) === (row.is_void === null),
"Native parameter type/void knowledge mismatch",
);
requireFact(
row.is_void !== true || (row.size_bytes === 0 && row.storage === null),
"Native void parameter has invented storage",
);
};
export const readParameterFacet = (fn) =>
readNativeFacet(fn, "atomic_parameters", "function parameters", (raw) => {
const facts = parse(raw);
facts.items.forEach(validate);
validate(facts.output, null);
requireFact(
(facts.model === null) === (facts.model_unknown === null),
"Native prototype model knowledge mismatch",
);
return facts;
});
const defaultFields = [
"storage",
"size_bytes",
"is_void",
"type_locked",
"indirect_storage",
"hidden_return",
"this_pointer",
];
const identity = ["id", "native_index"];
export function createParameterViews(facts) {
freezeFacts(facts);
const { schema_version: _version, items, output, ...prototype } = facts;
freezeFacts(prototype);
const coverage = freezeFacts({
scope: "all entries in the captured native recovered function prototype",
source_signature: "unknown",
source_parameter_ordinals: "unknown",
raw_input_values:
"is_input is not formal-parameter membership; globals/stack may also be input varnodes",
ssa_binding:
"not collected here; exact selected storage/width lookup is separate and does not prove aliases",
runtime_binding: "unknown",
machine_semantic_equivalence: "unknown",
input_recovery_errors: prototype.input_errors,
output_recovery_errors: prototype.output_errors,
});
return {
count: items.length,
parameters: (input) => {
const indices =
input.indices === undefined ? null : [...new Set(input.indices)];
const rows = indices
? indices.flatMap((index) => items[index] ?? [])
: items;
const fields = input.fields ?? defaultFields;
return {
...nativePage(rows, { ...input, fields }, identity),
missing_indices:
indices?.filter((index) => index >= items.length) ?? [],
output: projectFact(output, fields, identity),
prototype,
coverage,
};
},
};
}
@@ -0,0 +1,171 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
readParameterFacet,
createParameterViews,
} from "./native-parameters.mjs";
import { parseNativeIR } from "./native-ir.mjs";
import { methods, reference } from "./api.mjs";
import Ajv2020 from "ajv/dist/2020.js";
const parameter = (
index,
storage = { space: "register", offset: "0x38", size_bytes: 8 },
) => ({
id: index === null ? "return" : `parameter:${index}`,
native_index: index,
name: "display",
type_name: "unknown8",
size_bytes: 8,
is_void: false,
storage,
type_locked: false,
name_locked: false,
size_locked: false,
this_pointer: false,
indirect_storage: false,
hidden_return: false,
});
const fixture = () => ({
schema_version: 1,
model: "__stdcall",
model_unknown: false,
inputs_locked: false,
output_locked: false,
variadic: false,
input_errors: false,
output_errors: true,
items: [
parameter(0),
parameter(1, { space: "stack", offset: "0x8", size_bytes: 8 }),
],
output: parameter(null, { space: "register", offset: "0x0", size_bytes: 8 }),
});
const read = (raw) => readParameterFacet({ atomic_parameters: raw });
const model = (fn) =>
parseNativeIR(
JSON.stringify({
binary: "/input",
count: 1,
language: "c",
error: null,
functions: [
{ address_hex: "0x1000", error: null, code: "readable", ...fn },
],
}),
"0x1000",
);
test("formal selection preserves native indices, caller order, unknowns and independently selected output", () => {
const parsed = read(fixture());
assert.equal(parsed.ok, true);
const view = createParameterViews(parsed.value);
const first = view.parameters({
indices: [1, 0, 1, 8],
fields: ["storage", "size_bytes"],
limit: 1,
});
assert.deepEqual(
first.items.map((r) => r.id),
["parameter:1"],
);
assert.equal(first.total, 2);
assert.equal(first.next_offset, 1);
assert.deepEqual(first.missing_indices, [8]);
assert.equal(first.output.id, "return");
assert.equal(first.prototype.output_errors, true);
assert.equal(first.coverage.source_signature, "unknown");
const next = view.parameters({ indices: [1, 0], fields: [], offset: 1 });
assert.deepEqual(next.items, [{ id: "parameter:0", native_index: 0 }]);
assert.deepEqual(next.output, { id: "return", native_index: null });
assert.deepEqual(view.parameters({ indices: [] }).items, []);
assert.equal(Object.hasOwn(view.parameters({}).items[0], "type_name"), false);
});
test("cached metadata/storage cannot be changed by SDK callers", () => {
const view = createParameterViews(read(fixture()).value);
const result = view.parameters({});
assert.throws(() => {
result.prototype.model = "changed";
}, TypeError);
assert.throws(() => {
result.items[0].storage.offset = "0x0";
}, TypeError);
result.items.length = 0;
assert.equal(view.parameters({}).total, 2);
assert.equal(view.parameters({}).prototype.model, "__stdcall");
});
test("unknown type/storage is distinct from native void, malformed identities cannot publish partial formals", () => {
const unknown = fixture();
Object.assign(unknown.output, {
type_name: null,
is_void: null,
storage: null,
});
assert.equal(read(unknown).ok, true);
const empty = fixture();
empty.items = [];
Object.assign(empty.output, {
type_name: "void",
is_void: true,
size_bytes: 0,
storage: null,
});
assert.equal(read(empty).ok, true);
for (const change of [
(x) => (x.items[0].id = "parameter:9"),
(x) => (x.items[1].native_index = 0),
(x) => (x.output.native_index = 0),
(x) => (x.items[0].size_bytes = 4),
(x) => (x.output.is_void = true),
(x) => (x.output.type_name = null),
(x) => (x.model_unknown = null),
]) {
const x = fixture();
change(x);
assert.equal(read(x).ok, false);
}
});
test("prototype facet works without graph/calls; unavailable/invalid prototype retains code and reason", () => {
const available = model({
atomic_parameters: fixture(),
atomic_facts_error: "capacity",
atomic_calls_error: "capacity",
});
assert.equal(available.facets.parameters.availability, "available");
assert.equal(available.parameters({}).total, 2);
assert.equal(available.facets.recovered_ir.availability, "unavailable");
assert.equal(available.code, "readable");
for (const fn of [
{},
{
atomic_parameters_error: "native function prototype store is unavailable",
},
{ atomic_parameters: { schema_version: 99 } },
]) {
const unavailable = model(fn);
assert.equal(unavailable.code, "readable");
assert.equal(unavailable.facets.parameters.availability, "unavailable");
assert.throws(
() => unavailable.parameters({}),
(e) => e.details.facet === "parameters",
);
}
});
test("parameter input schema matches advertised JSON Schema and rejects guessed keys/indices", () => {
const validate = new Ajv2020({ strict: false }).compile(
reference("parameters").methods[0].input_schema,
);
for (const [request, valid] of [
[{}, true],
[{ indices: [], fields: [] }, true],
[{ indices: [1, 0, 1], fields: ["storage", "type_name"] }, true],
[{ indices: [-1] }, false],
[{ indices: ["0"] }, false],
[{ fields: ["value_id"] }, false],
[{ signature: true }, false],
]) {
const input = { snapshot: "snap_test", ...request };
assert.equal(methods.parameters.input.safeParse(input).success, valid);
assert.equal(validate(input), valid);
}
});
@@ -0,0 +1,10 @@
// Shared native address/storage syntax; caller/callee interpretation stays separate.
import { z } from "zod";
import { hex } from "./native-contract.mjs";
export const nativeLocation = z.strictObject({
space: z.string().min(1),
offset: hex,
});
export const nativeStorage = nativeLocation.extend({
size_bytes: z.number().int().positive().max(0x7fffffff),
});
+13 -10
View File
@@ -292,6 +292,7 @@ export class KunaFactOwner {
availability: "available",
scope: "diagnostic-printed-SSA",
},
parameters: missingNativeFacet(),
calls: missingNativeFacet(),
values: missingNativeFacet(),
},
@@ -421,32 +422,34 @@ export class KunaFactOwner {
: {},
);
}
async values(input) {
#queryNative(input, read) {
this.#requireNativeIR(input.snapshot);
return this.#query(input, (model) => model.values(input));
return this.#query(input, read);
}
async values(input) {
return this.#queryNative(input, (model) => model.values(input));
}
async calls(input) {
this.#requireNativeIR(input.snapshot);
return this.#query(input, (model) => model.calls(input));
return this.#queryNative(input, (model) => model.calls(input));
}
async switches(input) {
this.#requireNativeIR(input.snapshot);
return this.#query(input, (model) => model.switches(input));
return this.#queryNative(input, (model) => model.switches(input));
}
async blocks(input) {
this.#requireNativeIR(input.snapshot);
return this.#query(input, (model) => ({
return this.#queryNative(input, (model) => ({
...model.blocks(input),
coverage: model.coverage,
}));
}
async path(input) {
this.#requireNativeIR(input.snapshot);
return this.#query(input, (model) => ({
return this.#queryNative(input, (model) => ({
...model.path(input),
coverage: model.coverage,
}));
}
async parameters(input) {
return this.#queryNative(input, (model) => model.parameters(input));
}
async code(input) {
const descriptor = this.#snapshots.get(input.snapshot);
if (!descriptor)
+3 -1
View File
@@ -24,7 +24,7 @@ const returns = {
references:
"data:{results:[{ok,address,source:{evidence_id},items:[{source_row,from,to,kind,fromFunction:{address,name}|null,toFunction:{address,name}|null,instruction}],total,next_offset,coverage}|{ok:false,address,error}]}. source_row is the zero-based row in retained native xrefs; Evidence identity plus source_row identifies an occurrence. Coverage preserves examined/excluded/unattributed counts and unknown function scope/discovery. fields projects optional columns; endpoints/kind/source_row stay.",
snapshot:
"data:{snapshot:string,entry,source:{evidence_id,sha256},facets:{code:{availability},recovered_ir:{availability,error?},operations:{availability,scope?,count?,root_count?,definition_frontier_count?,error?},calls:{availability,count?,by_opcode?:[{opcode,known_target_count,unknown_target_count}],coverage?,error?},values:{availability,scope?,count?,error?},switches?:{availability,count?,table_count?,partial_table_count?,case_count?,relation_count?,error?}},coverage}. Call counts cover all retained recovered calls, including is_dead rows, not complete machine/runtime calls; target knowledge comes from a non-null native target, independent of opcode. An unavailable facet omits counts; an available empty call facet reports zeros. The complete graph can be unavailable while operations reports a bounded projection count; an unavailable operation facet has unknown/null counts, never a fabricated empty set; code remains readable through the same handle.",
"data:{snapshot:string,entry,source:{evidence_id,sha256},facets:{code:{availability},parameters?:{availability,count?,error?},recovered_ir:{availability,error?},operations:{availability,scope?,count?,root_count?,definition_frontier_count?,error?},calls:{availability,count?,by_opcode?:[{opcode,known_target_count,unknown_target_count}],coverage?,error?},values:{availability,scope?,count?,error?},switches?:{availability,count?,table_count?,partial_table_count?,case_count?,relation_count?,error?}},coverage}. Call counts cover all retained recovered calls, including is_dead rows, not complete machine/runtime calls; target knowledge comes from a non-null native target, independent of opcode. An unavailable facet omits counts; an available empty call facet reports zeros. The complete graph can be unavailable while operations reports a bounded projection count; an unavailable operation facet has unknown/null counts, never a fabricated empty set; code remains readable through the same handle.",
operations:
"data:{items:[{id,origin_address,block,is_dead,kind,opcode,output,input_count,definition_count,constant_count}],total,next_offset,evidence_id,coverage}. Native defaults are compact summaries; fields optionally selects statement,inputs,definitions,constants or summary columns; identity/is_dead stay. Native inputs:[{input_index,value_id,phi_predecessor,phi_predecessor_edge}], output:value_id|null; constants:[{value_id,input_index,value,printed_size_bytes,is_annotation}] retain exact u64 offsets, not arbitrary-width computed values; definitions:[{definition,use,input_index,value_id,phi_predecessor,phi_predecessor_edge}]. Page large input lists with relations kind:operands. Diagnostic rows retain legacy printed statements/occurrences and omit unavailable native counts. Projection scope is partial, with unknown CFG and definition frontier coverage.",
relations:
@@ -36,6 +36,8 @@ const returns = {
blocks:
"data:{items:[{id,origin_start,operations,predecessors,successors:[{target,index,polarity,condition_value,condition_test}]}],total,next_offset,evidence_id,coverage}; IDs are local to this snapshot.",
path: "data:{status:found|not-found|unknown,from,target:{kind:block,id}|{kind:exit},avoid,end:number|null,items:[native CFG edge with id,from,to,index,operation,polarity,condition_value,condition_test,step_index],total:number|null,next_offset,unretained_blocks,path_coverage:{scope,search,examined_blocks,examined_edges,branch_predicate_feasibility:unknown,machine_semantic_equivalence:unknown,runtime_reachability:unknown},evidence_id,coverage}. total is full witness edge count before paging (zero for found zero-edge/not-found, null for unknown selection); end identifies the terminal block even on an empty page. step_index is the path position; index remains native successor ordinal. Unknown selections are never cached as no-path; absence only covers recovered edges under exclusions. Edges retain source identities and the complete snapshot Evidence remains readable.",
parameters:
"data:{items:[{id:parameter:N,native_index:N,storage?,size_bytes?,is_void?,type_locked?,indirect_storage?,hidden_return?,this_pointer?,name?,type_name?,name_locked?,size_locked?}],output:{id:return,native_index:null,...same selected fields},prototype:{model,model_unknown,inputs_locked,output_locked,variadic,input_errors,output_errors},total,next_offset,missing_indices,coverage,evidence_id}. Entries are native recovered formals, not raw is_input varnodes or proven source ordinals. Exact source unknowns and lock/error flags remain. indices deduplicate in caller order; empty indices selects none but preserves output/prototype. total counts selected entries before paging, missing_indices reports absent native slots. Output storage absence alone is not void; is_void:null is unknown. No automatic inter-function SSA equality or stack/alias interpretation is asserted.",
slice:
"data:{operations:[operation rows as above],coverage:{status,boundaries:[{id,reason}],missing,missing_reasons?,source_scope?,source_issues,runtime_reachability,memory_alias_dependencies},value_frontier?:[{value_id?|operation?,input_index?,reason}],evidence_id}",
code: "data:{evidence_id,start_line,language:string|null,text,total_lines,next_offset,coverage}; language is native-reported or unknown. Read/filter full reconstructed text within Code Mode without returning it all.",
@@ -35,7 +35,7 @@ test("reference index discovers named methods without constructing unrequested s
/Unknown reference view/,
);
const all = reference("", { view: "index" });
assert.equal(all.methods.length, 16);
assert.equal(all.methods.length, 17);
assert.ok(
all.methods.some(
(m) => m.method === "relations" && m.description.includes("operands"),
@@ -75,7 +75,7 @@ test("named reference lists preserve caller order and exact schemas across prese
["facts_values", "facts_snapshot"],
);
assert.deepEqual(result.unmatched_queries, ["unknown"]);
assert.equal(result.coverage.declared_methods, 16);
assert.equal(result.coverage.declared_methods, 17);
assert.equal(result.coverage.matched_methods, 2);
assert.equal(result.coverage.selection, "named");
const javascript = reference("rea.values snapshot");
@@ -104,7 +104,7 @@ test("named reference lists preserve caller order and exact schemas across prese
});
test("topic/all reference selection and shared schema ownership remain explicit", () => {
assert.equal(reference().methods.length, 16);
assert.equal(reference().methods.length, 17);
assert.equal(reference("no-such-topic").coverage.matched_methods, 0);
assert.ok(
reference("memory aliases").methods.some((m) => m.method === "slice"),
@@ -53,6 +53,7 @@ for (const direct of [false, true]) {
const ids = [
"switches",
"path",
"parameters",
"snapshot",
"operations",
"relations",
@@ -67,7 +68,7 @@ for (const direct of [false, true]) {
ids,
);
assert.deepEqual(selected.unmatched_queries, []);
assert.equal(selected.coverage.declared_methods, 16);
assert.equal(selected.coverage.declared_methods, 17);
const ajv = new Ajv2020({ strict: false });
for (const tool of catalog.tools) ajv.compile(tool.inputSchema);
assert.equal(Object.hasOwn(selected, "limits"), !direct);
@@ -169,6 +170,15 @@ for (const direct of [false, true]) {
input_indices: [0],
},
],
[
"parameters",
{
snapshot: "snap_other",
indices: [0, 1],
fields: ["storage", "is_void"],
},
{ snapshot: "snap_other", indices: ["0"] },
],
[
"evidence",
{ evidence_id: "ev_example", component: "recovered_ir" },
@@ -206,7 +216,7 @@ for (const direct of [false, true]) {
);
assert.deepEqual(missing.unmatched_queries, ["unknown"]);
const all = await call("");
assert.equal(all.methods.length, 16);
assert.equal(all.methods.length, 17);
for (const method of all.methods) {
ajv.compile(method.input_schema);
assert.equal(method.description, methods[method.method].description);
@@ -0,0 +1,208 @@
// Real public boundary facts; the complete old record remains a fidelity oracle.
import assert from "node:assert/strict";
import { readFile, writeFile } from "node:fs/promises";
import { resolve, join } from "node:path";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { familyCounters } from "./family-counters.mjs";
import {
createVerifierRun,
completeVerifierRun,
assertVerifierCleanupComplete,
} from "../../scripts/lib/verifier-run.mjs";
const [output] = process.argv.slice(2);
assert.ok(output && process.env.REA_ATOMIC_EVIDENCE_DIR);
const cases = [
[
"0x25cb0",
"ev_36a9ed8af203f2f64ca1a60d51b791f3c52e81fc1aa0e895aa183d99fd3ceba9",
],
[
"0x280c0",
"ev_007ef9474700eff262536cfc0ff6e0187afaf50990888cc446bc78fa58538f65",
],
];
const fields = [
"name",
"type_name",
"storage",
"size_bytes",
"is_void",
"type_locked",
"name_locked",
"size_locked",
"this_pointer",
"indirect_storage",
"hidden_return",
];
const run = createVerifierRun(),
calls = [],
phases = [],
reviews = [],
failures = [];
const direct = process.env.REA_ATOMIC_ADAPTER === "direct";
const client = new Client({ name: "rea-parameter-verifier", version: "0.1.0" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("experiments/atomic-codemode/server.mjs")],
env: { ...process.env },
stderr: "pipe",
});
const call = async (name, input) => {
const reply = await client.callTool(
{ name, arguments: input },
{ timeout: 180000 },
);
const result = JSON.parse(reply.content[0].text);
calls.push({
name,
input,
result,
response_bytes: Buffer.byteLength(reply.content[0].text),
});
assert.equal(result.ok, true, JSON.stringify(result));
return result;
};
const read = async (name, input) => {
const result = direct
? await call(`facts_${name}`, input)
: (
await call("execute_javascript", {
code: `return await rea.${name}(${JSON.stringify(input)});`,
})
).value;
assert.equal(result.ok, true, JSON.stringify(result));
return result.data;
};
const measure = async (phase, work) => {
const before = await familyCounters(),
start = performance.now(),
first = calls.length;
const result = await work(),
after = await familyCounters();
phases.push({
phase,
elapsed_ms: performance.now() - start,
family_cpu_seconds: (after.cpu - before.cpu) / 1e6,
cumulative_family_memory_peak_bytes: after.peak,
mcp_calls: calls.length - first,
response_bytes: calls
.slice(first)
.reduce((sum, c) => sum + c.response_bytes, 0),
});
return result;
};
try {
await client.connect(transport);
await call("open_binary", { path: "/usr/bin/ssh" });
for (const [entry, prior] of cases) {
const capture = await measure(`capture:${entry}`, () =>
read("snapshot", { entry }),
);
assert.equal(
capture.facets.parameters.availability,
"available",
JSON.stringify(capture),
);
const snapshot = capture.snapshot;
const full = await measure(`parameters-first:${entry}`, () =>
read("parameters", { snapshot, fields, limit: 128 }),
);
const repeat = await measure(`parameters-repeat:${entry}`, () =>
read("parameters", { snapshot, fields, limit: 128 }),
);
assert.deepEqual(repeat, full);
assert.equal(full.next_offset, null);
assert.equal(full.items.length, capture.facets.parameters.count);
assert.equal(full.coverage.source_signature, "unknown");
const original = JSON.parse(
await readFile(
join(
process.env.REA_ATOMIC_EVIDENCE_DIR,
`${capture.source.evidence_id}.txt`,
),
"utf8",
),
).functions[0];
const { atomic_parameters: raw, ...ordinary } = original;
const golden = JSON.parse(
await readFile(
resolve(".cache/atomic-codemode/ssh-guard-v1-evidence", `${prior}.txt`),
"utf8",
),
).functions[0];
assert.deepEqual(ordinary, golden);
assert.deepEqual(full.items, raw.items);
assert.deepEqual(full.output, raw.output);
const {
schema_version: _version,
items: _items,
output: _output,
...prototype
} = raw;
assert.deepEqual(full.prototype, prototype);
const selected = await read("parameters", {
snapshot,
indices: [raw.items.length - 1, 0, 0, raw.items.length + 1],
fields: [],
limit: 1,
});
assert.equal(selected.total, Math.min(raw.items.length, 2));
assert.deepEqual(selected.missing_indices, [raw.items.length + 1]);
assert.deepEqual(selected.output, { id: "return", native_index: null });
const empty = await read("parameters", {
snapshot,
indices: [],
fields: ["storage", "is_void"],
});
assert.deepEqual(empty.items, []);
const rawInputs = await read("values", {
snapshot,
is_input: true,
limit: 1,
});
const locations = raw.items.flatMap((p) => (p.storage ? [p.storage] : []));
const matches = await read("values", {
snapshot,
is_input: true,
locations,
limit: 128,
});
reviews.push({
entry,
source: capture.source,
prototype: full.prototype,
items: full.items,
output: full.output,
raw_input_value_count: rawInputs.total,
formal_storage_input_matches: matches.items,
unretained_formal_locations: matches.unretained_locations,
complete_previous_function_equal_except_added_parameters: true,
});
}
await call("close_binary", {});
} catch (error) {
failures.push(error.stack ?? String(error));
} finally {
await client.close();
await transport.close();
}
const lifecycle = await completeVerifierRun(run);
assertVerifierCleanupComplete(lifecycle.process_lineage);
await writeFile(
output,
JSON.stringify({
direct,
phases,
calls,
reviews,
failures,
lifecycle,
scope:
"Actual SDK MCP and complete retained native fidelity; recovered prototypes are not source signatures or ABI/runtime proof. No model cost claim.",
}),
{ flag: "wx" },
);
console.log(JSON.stringify({ output, direct, phases, reviews, failures }));
if (failures.length) process.exitCode = 1;