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:
1 parent
9a769cbefe
commit
5ca00675b7
18 files changed
+1091
-96
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user