Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions crates/fission-cli/src/cli/oneshot/rust_decomp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ struct RustSleighRender {
/// The variables this decompilation recovered, for a consumer that wants
/// them as data rather than as printed declarations.
variables: Vec<fission_decompiler::RecoveredVariable>,
nir_line_mappings: Vec<fission_decompiler::RecoveredLineMapping>,
hir_line_mappings: Vec<fission_decompiler::RecoveredLineMapping>,
}

fn render_with_rust_sleigh(
Expand Down Expand Up @@ -88,6 +90,16 @@ fn render_with_rust_sleigh(
.and_then(|output| output.recovered_variables.clone())
.unwrap_or_default();

let layers = result
.render_output
.as_ref()
.and_then(|output| output.layered.as_ref());
let nir_line_mappings = layers
.map(|layers| layers.nir_line_mappings.clone())
.unwrap_or_default();
let hir_line_mappings = layers
.map(|layers| layers.hir_line_mappings.clone())
.unwrap_or_default();
Ok(RustSleighRender {
code: result.code,
code_nir: result.code_nir,
Expand All @@ -99,6 +111,8 @@ fn render_with_rust_sleigh(
hint_stats: result.hint_stats,
evidence: result.evidence,
variables,
nir_line_mappings,
hir_line_mappings,
})
}

Expand All @@ -113,6 +127,66 @@ fn apply_output_filters(code: &str, config: RenderConfig) -> String {
filtered
}

fn remap_retained_lines(
original: &str,
filtered: &str,
mappings: &[fission_decompiler::RecoveredLineMapping],
) -> Vec<fission_decompiler::RecoveredLineMapping> {
let source = original.lines().collect::<Vec<_>>();
let mut cursor = 0;
let mut occurrences = std::collections::BTreeMap::new();
for line in &source {
*occurrences.entry(*line).or_insert(0usize) += 1;
}
let mut retained = std::collections::BTreeMap::new();
for (new_index, line) in filtered.lines().enumerate() {
while cursor < source.len() && source[cursor] != line {
cursor += 1;
}
if cursor == source.len() {
return Vec::new();
}
retained.insert(cursor + 1, new_index + 1);
cursor += 1;
}
mappings
.iter()
.filter_map(|mapping| {
let original_line = *source.get(mapping.line_number.checked_sub(1)?)?;
if occurrences.get(original_line) != Some(&1) {
return None;
}
let line_number = *retained.get(&mapping.line_number)?;
Some(fission_decompiler::RecoveredLineMapping {
line_number,
..mapping.clone()
})
})
.collect()
}

#[cfg(test)]
mod line_mapping_tests {
use super::*;
#[test]
fn filters_remap_retained_lines_and_reject_text_rewrites() {
let map = fission_decompiler::RecoveredLineMapping {
line_number: 3,
addresses: vec![0x1000],
defined_variables: vec!["value".into()],
};
let original = "WARNING: detail\n{\nvalue = 3;\n}";
let filtered = strip::strip_warnings(original);
assert_eq!(
remap_retained_lines(original, &filtered, &[map.clone()])[0].line_number,
2
);
assert!(remap_retained_lines(original, "{\nvalue = 4;\n}", &[map.clone()]).is_empty());
let repeated = "{\nvalue = 3;\nvalue = 3;\n}";
assert!(remap_retained_lines(repeated, "{\nvalue = 3;\n}", &[map]).is_empty());
}
}

fn shared_fact_store(
binary: &LoadedBinary,
scan_signature_matches: bool,
Expand Down Expand Up @@ -218,6 +292,18 @@ pub(crate) fn render_one_function_inner(
match render_with_rust_sleigh(binary, facts, func, config.timeout_ms, config.prehir) {
Ok(rendered) => {
let decomp_sec = start.elapsed().as_secs_f64();
let nir_original = rendered.code_nir.as_deref().unwrap_or(&rendered.code);
let hir_original = rendered.code_hir.as_deref().unwrap_or(&rendered.code);
let nir_line_mappings = remap_retained_lines(
nir_original,
&apply_output_filters(nir_original, config),
&rendered.nir_line_mappings,
);
let hir_line_mappings = remap_retained_lines(
hir_original,
&apply_output_filters(hir_original, config),
&rendered.hir_line_mappings,
);
let code = apply_output_filters(&rendered.code, config);
// Dual surfaces from one IR build; fall back so JSON shape stays stable.
let code_nir =
Expand All @@ -243,6 +329,8 @@ pub(crate) fn render_one_function_inner(
hint_stats: rendered.hint_stats.clone(),
decomp_sec,
variables: rendered.variables,
nir_line_mappings,
hir_line_mappings,
},
};

Expand Down
2 changes: 2 additions & 0 deletions crates/fission-cli/src/cli/oneshot/rust_decomp/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub(crate) enum CliRustOutcome {
/// Recovered variables, so a consumer does not have to parse them
/// back out of the printed declarations.
variables: Vec<fission_decompiler::RecoveredVariable>,
nir_line_mappings: Vec<fission_decompiler::RecoveredLineMapping>,
hir_line_mappings: Vec<fission_decompiler::RecoveredLineMapping>,
},
AssemblyFallback {
fallback_code: String,
Expand Down
13 changes: 12 additions & 1 deletion crates/fission-cli/src/cli/oneshot/rust_decomp/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ fn layered_text(
) -> String {
let nir = code_nir.unwrap_or(code).to_string();
let hir = code_hir.unwrap_or(code).to_string();
LayeredPseudocode { nir, hir }.format_text(layer, true)
LayeredPseudocode {
nir,
hir,
..Default::default()
}
.format_text(layer, true)
}

pub(crate) fn record_to_json(entry: &CliRustDecompileRecord, benchmark: bool) -> serde_json::Value {
Expand All @@ -33,6 +38,8 @@ pub(crate) fn record_to_json(entry: &CliRustDecompileRecord, benchmark: bool) ->
hint_stats,
decomp_sec,
variables,
nir_line_mappings,
hir_line_mappings,
} => {
let mut obj = serde_json::json!({
"address": addr_str,
Expand All @@ -46,6 +53,10 @@ pub(crate) fn record_to_json(entry: &CliRustDecompileRecord, benchmark: bool) ->
"fell_back": fell_back,
"fallback_reason": fallback_reason,
"variables": variables,
"line_mappings": if entry.layer == PseudocodeLayer::Hir { hir_line_mappings } else { nir_line_mappings },
"line_mappings_nir": nir_line_mappings,
"line_mappings_hir": hir_line_mappings,
"line_mapping_source": "fission_ssa_assignment_definition",
});
// Only present when `decomp --prehir` was passed -- absence means
// "not requested", not "structuring produced nothing".
Expand Down
2 changes: 1 addition & 1 deletion crates/fission-pcode/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ pub use midend::{
summarize_direct_tail_wrapper_from_pcode,
};
pub use pcode::optimizer::{PcodeOptimizer, PcodeOptimizerConfig};
pub use render::{RecoveredVariable, recovered_variables};
pub use render::{RecoveredLineMapping, RecoveredVariable, recovered_variables};
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ impl<'a> PreviewBuilder<'a> {
)
}

pub(crate) fn recovered_single_definition_addresses(&self) -> HashMap<String, u64> {
single_definition_addresses(
self.pcode,
&self.scalar_ssa,
&self.materialized_vns,
&self.explicit_merge_bindings,
)
}

/// Diagnostic-only: see module docs. Pure/read-only, `FISSION_PREVIEW_DIAG`-gated caller.
pub(crate) fn scan_cover_violations(&self) -> Vec<CoverViolation> {
scan_cover_violations(
Expand Down Expand Up @@ -386,6 +395,76 @@ fn instruction_addresses_by_materialized_name(
.collect()
}

/// A result's defining instruction, never its entire def/use lifetime.
fn single_definition_addresses(
pcode: &PcodeFunction,
scalar_ssa: &NirScalarSsa,
materialized_vns: &HashMap<MaterializedVarnodeKey, String>,
explicit_merge_bindings: &HashMap<(usize, VarnodeKey), String>,
) -> HashMap<String, u64> {
use fission_midend_core::ir::SsaValueDefinition;
let admitted = instruction_addresses_by_materialized_name(
pcode,
scalar_ssa,
materialized_vns,
explicit_merge_bindings,
);
let joins = explicit_merge_bindings
.values()
.collect::<std::collections::BTreeSet<_>>();
let mut sites = HashMap::default();
for (block_idx, block) in pcode.blocks.iter().enumerate() {
for (op_idx, op) in block.ops.iter().enumerate() {
let key = (op.address, op.seq_num);
let site = LoweringSite { block_idx, op_idx };
sites
.entry(key)
.and_modify(|value| *value = None)
.or_insert(Some(site));
}
}
let mut output = HashMap::default();
let mut rejected = std::collections::BTreeSet::new();
for (key, name) in materialized_vns {
if !admitted.contains_key(name) || joins.contains(name) {
continue;
}
let Some(Some(site)) = sites.get(&(key.def_addr, key.def_seq)) else {
rejected.insert(name.clone());
continue;
};
let Some(high_id) = high_variable_at_output(scalar_ssa, *site, &key.varnode) else {
rejected.insert(name.clone());
continue;
};
let Some(high) = scalar_ssa.high_variables.get(high_id.0 as usize) else {
continue;
};
let proved = !high.members.is_empty()
&& high.members.iter().all(|member| {
let Some(value) = scalar_ssa.values.get(member.0 as usize) else {
return false;
};
let SsaValueDefinition::Operation(definition) = value.definition else {
return false;
};
pcode_instruction_address(pcode, definition.block, definition.op)
== Some(key.def_addr)
});
if !proved
|| output
.get(name)
.is_some_and(|address| *address != key.def_addr)
{
rejected.insert(name.clone());
} else {
output.insert(name.clone(), key.def_addr);
}
}
output.retain(|name, _| !rejected.contains(name));
output
}

/// Scan every rendered binding name for cases where it was assigned to more
/// than one interfering `SsaHighVariable`. Pure/read-only: does not touch
/// `materialized_vns`/`explicit_merge_bindings`/output.
Expand Down Expand Up @@ -654,6 +733,44 @@ mod tests {
assert_eq!(addresses.get("source_local"), Some(&vec![0x1000, 0x1004]));
}

#[test]
fn single_definition_projection_rejects_inputs_phis_and_cloned_sites() {
let mut ssa = sample_ssa_with_two_high_variables(false);
let mut pcode = sample_pcode_two_defs(4, 0x10, 4);
let key = MaterializedVarnodeKey {
varnode: VarnodeKey {
space_id: 4,
offset: 0x10,
size: 4,
is_constant: false,
constant_val: 0,
},
def_addr: 0x1000,
def_seq: 0,
};
let materialized = HashMap::from_iter([(key, "value".to_string())]);
assert!(
single_definition_addresses(&pcode, &ssa, &materialized, &HashMap::default())
.is_empty()
);
ssa.values[0].definition = SsaValueDefinition::Operation(SsaOpSite { block: 0, op: 0 });
assert_eq!(
single_definition_addresses(&pcode, &ssa, &materialized, &HashMap::default())["value"],
0x1000
);
ssa.values[0].definition = SsaValueDefinition::Phi { block: 0 };
assert!(
single_definition_addresses(&pcode, &ssa, &materialized, &HashMap::default())
.is_empty()
);
ssa.values[0].definition = SsaValueDefinition::Operation(SsaOpSite { block: 0, op: 0 });
pcode.blocks.push(pcode.blocks[0].clone());
assert!(
single_definition_addresses(&pcode, &ssa, &materialized, &HashMap::default())
.is_empty()
);
}

#[test]
fn instruction_provenance_drops_a_name_reused_for_distinct_values() {
let ssa = sample_ssa_with_two_high_variables(false);
Expand Down
18 changes: 16 additions & 2 deletions crates/fission-pcode/src/midend/orchestrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,13 @@ fn render_mlil_preview_dual_layer_output(
// through the presentation pass. NIR remains the scored tree's
// semantic-faithful print, while HIR comes from the jump-minimizing tree
// and is allowed to optimize for readability.
let layered = stitch_dual_layers(&scored_layers, hir);
let hir_mappings = output
.layered
.as_ref()
.map(|layers| layers.hir_line_mappings.clone())
.unwrap_or_default();
let mut layered = stitch_dual_layers(&scored_layers, hir);
layered.hir_line_mappings = hir_mappings;
output.code = scored.code;
output.layered = Some(layered.clone());
Ok(output)
Expand All @@ -218,6 +224,8 @@ fn stitch_dual_layers(
// even though `scored.code` still came from NIR.
LayeredPseudocode {
nir: scored_layers.nir.clone(),
nir_line_mappings: scored_layers.nir_line_mappings.clone(),
hir_line_mappings: Vec::new(),
hir: readable_hir,
}
}
Expand Down Expand Up @@ -596,7 +604,12 @@ fn render_mlil_preview_with_binary_and_context_output(
// Always build dual NIR/HIR surfaces from one structured tree. Callers that
// only need a single string use `LayeredPseudocode::primary` / legacy
// `render_nir` which returns the NIR-faithful surface for oracle compat.
let layered = render_layered_pseudocode(&hir, options);
let definitions = builder
.recovered_single_definition_addresses()
.into_iter()
.collect();
let layered =
crate::render::render_layered_pseudocode_with_provenance(&hir, options, &definitions);
let rendered = layered.nir.clone();
record_ghidra_action_stage(&mut build_stats, GhidraActionConcept::PrintC);
record_ghidra_clean_room_pipeline_complete(&mut build_stats);
Expand Down Expand Up @@ -761,6 +774,7 @@ mod tests {
let scored = LayeredPseudocode {
nir: "(unsigned long long)(uint)lane".into(),
hir: "lane".into(),
..Default::default()
};

let layered = stitch_dual_layers(&scored, "readable lane".into());
Expand Down
13 changes: 13 additions & 0 deletions crates/fission-pcode/src/render/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@

use serde::{Deserialize, Serialize};

/// A proved contributing result definition on an actual output line.
/// It does not assert provenance for every RHS occurrence on that line.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoveredLineMapping {
pub line_number: usize,
pub addresses: Vec<u64>,
pub defined_variables: Vec<String>,
}

/// Which pseudocode surface to emit (CLI / JSON primary selection).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -69,6 +78,10 @@ pub struct LayeredPseudocode {
pub nir: String,
/// Human-readable presentation.
pub hir: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub nir_line_mappings: Vec<RecoveredLineMapping>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hir_line_mappings: Vec<RecoveredLineMapping>,
}

impl LayeredPseudocode {
Expand Down
Loading
Loading