From 9bcfe9ddd9748a6ae09eb22c7bf79f14d186af41 Mon Sep 17 00:00:00 2001 From: Paulo Matias Date: Thu, 22 Oct 2026 16:09:53 -0300 Subject: [PATCH 01/11] [AUTOMATED] Preserve SPARC32 __divdi3 quotient results Seed the reserved runtime helper with its signed eight-byte DI-mode operands and result under libproto on the evidenced SPARC32 ELF ABI. Reuse code-import resolution and ambiguity filtering. Keep unknown external-call inference unchanged and retain explicit prototype precedence. Exercise low-word quotient stores and a scalar override by compiling and executing independently authored callers. --- .../src/analyzers/protos/kuna_libgcc.rs | 36 +++++ .../kuna-analysis/src/analyzers/protos/mod.rs | 12 +- .../crates/kuna-cli/tests/integration.rs | 3 + .../crates/kuna-cli/tests/libgcc_divdi3.rs | 129 ++++++++++++++++++ docs/spec/01-program-prep.md | 10 ++ 5 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs create mode 100644 decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs diff --git a/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs b/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs new file mode 100644 index 0000000000..766ae57c1a --- /dev/null +++ b/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs @@ -0,0 +1,36 @@ +//! Compiler-runtime contracts whose integer widths come from machine modes, +//! not the host C data model. Applied under the existing `libproto` option. +//! +//! `__divdi3` is `di_int (di_int, di_int)`, with `di_int = int64_t`: +//! https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/builtins/divdi3.c +//! https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/builtins/int_types.h + +use std::collections::HashSet; + +use object::{Architecture, BinaryFormat, Object}; + +use super::{seed_named_prototypes, seed_resolved_prototypes, Sig, Ty, L}; +use crate::pass::{AnalysisCtx, AnalysisOutput}; + +const SIGNATURES: &[(&str, Sig)] = &[ + ("__divdi3", Sig { ret: Ty::Int64, params: &[Ty::Int64, Ty::Int64], vararg: -1 }), +]; + +/// Seed the evidenced SPARC32 ELF runtime ABI without retyping other targets. +pub(super) fn seed( + ctx: &AnalysisCtx, + out: &mut AnalysisOutput, + names: &HashSet, + imports: &[(String, u64)], +) { + if ctx.file.format() != BinaryFormat::Elf + || !matches!(ctx.file.architecture(), Architecture::Sparc | Architecture::Sparc32Plus) + || ctx.arch.get_description() != "sparc:BE:32:default:default" + { + return; + } + let types = ctx.arch.types(); + let (_, word_size) = ctx.arch.data_org(); + seed_named_prototypes(out, names, SIGNATURES, types, word_size, L); + seed_resolved_prototypes(out, imports, SIGNATURES, types, word_size, L); +} diff --git a/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs b/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs index cd163a30e0..8b528f91cd 100644 --- a/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs +++ b/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs @@ -38,6 +38,7 @@ use kuna_decomp::fspec::PrototypePieces; use crate::pass::{AnalysisCtx, AnalysisOutput, AnalysisPass, Phase}; +mod kuna_libgcc; mod kuna_libcnames; pub mod kuna_libcsigs; pub mod kuna_libctypes; @@ -49,10 +50,9 @@ pub struct LibProtoPass; /// A primitive type slot in a built-in libc signature. /// -/// Every variant is **width-stable**: it is either `void`, exactly 4 bytes, or -/// exactly pointer-width on every ILP32/LP64 target. A C type that is neither -/// (`off_t`, `time_t`, `long long`, `char`/`short` parameters) has no spelling -/// here on purpose — see [`kuna_libcsigs`]. +/// Integer slots are four bytes or pointer-width for libc declarations. Compiler +/// runtime machine-mode contracts can additionally state an exact eight-byte +/// integer; that does not establish the width of libc `long long` parameters. #[derive(Clone, Copy)] enum Ty { /// `void` (return only). @@ -65,6 +65,8 @@ enum Ty { Size, /// `ssize_t` / `long` / `ptrdiff_t` (pointer-width signed). Long, + /// Signed DI-mode integer (exactly eight bytes in the compiler runtime ABI). + Int64, /// `char *`. CharPtr, /// `char **`. @@ -182,6 +184,7 @@ fn build_ty( Ty::UInt => types.get_base(4, type_metatype::TYPE_UINT), Ty::Size => types.get_base(ptr, type_metatype::TYPE_UINT), Ty::Long => types.get_base(ptr, type_metatype::TYPE_INT), + Ty::Int64 => types.get_base(8, type_metatype::TYPE_INT), Ty::CharPtr => { let c = types.get_type_char(types.get_size_of_char())?; types.get_type_pointer(ptr, c, word_size) @@ -607,6 +610,7 @@ impl AnalysisPass for LibProtoPass { let (_addr_size, word_size) = ctx.arch.data_org(); seed_named_prototypes(&mut out, &present, LIBC, types, word_size, L); seed_resolved_prototypes(&mut out, &imports, LIBC, types, word_size, L); + kuna_libgcc::seed(ctx, &mut out, &present, &imports); out.c_runtime_imports = crate::loader::pe_iat::c_runtime_import_names(ctx.bytes); out } diff --git a/decompiler/crates/kuna-cli/tests/integration.rs b/decompiler/crates/kuna-cli/tests/integration.rs index 92f4f5464e..72fdcab03d 100644 --- a/decompiler/crates/kuna-cli/tests/integration.rs +++ b/decompiler/crates/kuna-cli/tests/integration.rs @@ -218,6 +218,9 @@ mod local_stack_views; #[path = "library_names.rs"] mod library_names; +#[path = "libgcc_divdi3.rs"] +mod libgcc_divdi3; + #[path = "rettriangle_round_trip.rs"] mod rettriangle_round_trip; diff --git a/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs b/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs new file mode 100644 index 0000000000..f31fea08e7 --- /dev/null +++ b/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs @@ -0,0 +1,129 @@ +//! A named DI-mode helper's low return word remains a computed value. +use crate::common; +use object::{write, Architecture, BinaryFormat, Endianness, RelocationFlags, SymbolFlags, SymbolKind, SymbolScope}; +use std::process::Command; + +fn caller(low_word: bool) -> Vec { + let mut image = write::Object::new(BinaryFormat::Elf, Architecture::Sparc, Endianness::Big); + let text = image.section_id(write::StandardSection::Text); + // save; mov 62,o0; mov i0,o3; sra i0,31,o2; call; sethi 0x200000,o1; + // st o1,[i1]; ret; restore. Independently authored, with the call's delay slot. + let mut words: [u32; 9] = [0x9de3bfa0, 0x9010203e, 0x96100018, 0x953e201f, 0x40000000, + 0x13200000, 0xd2264000, 0x81c7e008, 0x81e80000]; + if !low_word { + words[6] = 0xd0264000; + } + let bytes: Vec = words.into_iter().flat_map(u32::to_be_bytes).collect(); + image.append_section_data(text, &bytes, 4); + image.add_symbol(write::Symbol { + name: b"divide_low".to_vec(), value: 0, size: bytes.len() as u64, + kind: SymbolKind::Text, scope: SymbolScope::Linkage, weak: false, + section: write::SymbolSection::Section(text), flags: SymbolFlags::None, + }); + let helper = image.add_symbol(write::Symbol { + name: b"__divdi3".to_vec(), value: 0, size: 0, kind: SymbolKind::Unknown, + scope: SymbolScope::Dynamic, weak: false, + section: write::SymbolSection::Undefined, flags: SymbolFlags::None, + }); + image.add_relocation(text, write::Relocation { + offset: 16, symbol: helper, addend: 0, + flags: RelocationFlags::Elf { r_type: object::elf::R_SPARC_WDISP30 }, + }).unwrap(); + image.write().unwrap() +} + +#[test] +fn signed_di_quotient_low_word_survives_an_opaque_call() { + let root = tempfile::tempdir().unwrap(); + let image = root.path().join("divide.o"); + std::fs::write(&image, caller(true)).unwrap(); + let compilers: Vec<_> = ["gcc", "clang"].into_iter() + .filter(|cc| Command::new(cc).arg("--version").output().is_ok()).collect(); + assert!(!compilers.is_empty(), "a C compiler is required"); + for mode in ["off", "on"] { + let (output, error, status) = common::run_kuna(&[ + "decompile", image.to_str().unwrap(), "divide_low", "--json", + "--option", "libproto", mode, + "--assert", "prototype divide_low void divide_low(int divisor,unsigned int *result)", + ]); + assert_eq!(status, 0, "{error}"); + let document: serde_json::Value = serde_json::from_str(&output).unwrap(); + assert!(document["functions"][0]["error"].is_null(), "{output}"); + let body = document["functions"][0]["code"].as_str().unwrap(); + let source = root.path().join(format!("{mode}.c")); + std::fs::write(&source, format!(r#" +#include +#include +long long __divdi3(long long a,long long b) {{ return a/b; }} +{body} +int main(void) {{ + const int divisors[] = {{1,-1,2,-2,3,-3,7,-7,999,-999,INT_MAX,INT_MIN}}; + for(unsigned i=0;i +#include +unsigned int __divdi3(unsigned int a,unsigned int b,unsigned int c,unsigned int d) {{ return a^b^c^d; }} +{body} +int main(void) {{ + const int inputs[] = {{0,1,-1,37,-37,INT_MAX,INT_MIN}}; + for(unsigned i=0;i 0, "a C compiler is required"); +} diff --git a/docs/spec/01-program-prep.md b/docs/spec/01-program-prep.md index d260fce3ce..84302cf7e5 100644 --- a/docs/spec/01-program-prep.md +++ b/docs/spec/01-program-prep.md @@ -1689,6 +1689,16 @@ moves. (stale i386 `-O0` outgoing stack slots, the AArch64 `x8` result register), and in an i386 PIC object, whose `GOTOFF` addresses the loader leaves unresolved, a `char *` parameter renders such a garbage address as a string literal. + The same `libproto` gate seeds the reserved compiler-runtime name `__divdi3` + on SPARC32 big-endian ELF under `sparc:BE:32:default:default` + (`decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs (seed)`). + Its DI-mode contract is a signed eight-byte quotient of two signed eight-byte + operands, independent of the target's four-byte C `long`. The normal compiler + model assigns the argument and return register pairs. Thus a caller storing + only `%o1` still stores the low word of the computed quotient instead of reading + an unassigned local. Other names and targets retain their existing inference. + Existing import-address and name-ambiguity rules apply, and DWARF or explicit + prototype assertions retain precedence over the built-in signature. - **(kuna) Measured libc signatures** (`libcsigs`, `decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libcsigs.rs (LibcSigsPass)`): the second, larger half of the same table, closing most of the LOSS above. Which From 8b922af099e5ee48004c7a42b2189a7eadf22bd5 Mon Sep 17 00:00:00 2001 From: Paulo Matias Date: Thu, 22 Oct 2026 21:35:30 -0300 Subject: [PATCH 02/11] [AUTOMATED] Preserve declared wide call results in caller returns --- .../crates/kuna-cli/tests/libgcc_divdi3.rs | 262 +++++++++++++++--- .../kuna-decomp/src/p3_dataflow/heritage.rs | 43 ++- .../src/p4_calls/coreaction_protos.rs | 16 +- .../src/p4_calls/kuna_retcallhalf.rs | 192 ++++++++++++- docs/spec/04-calls-and-prototypes.md | 20 ++ 5 files changed, 480 insertions(+), 53 deletions(-) diff --git a/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs b/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs index f31fea08e7..dc2080a6d9 100644 --- a/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs +++ b/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs @@ -1,6 +1,9 @@ -//! A named DI-mode helper's low return word remains a computed value. +//! Signed DI-mode call results survive stores and full-width caller returns. use crate::common; -use object::{write, Architecture, BinaryFormat, Endianness, RelocationFlags, SymbolFlags, SymbolKind, SymbolScope}; +use object::{ + write, Architecture, BinaryFormat, Endianness, RelocationFlags, SymbolFlags, SymbolKind, + SymbolScope, +}; use std::process::Command; fn caller(low_word: bool) -> Vec { @@ -8,27 +11,48 @@ fn caller(low_word: bool) -> Vec { let text = image.section_id(write::StandardSection::Text); // save; mov 62,o0; mov i0,o3; sra i0,31,o2; call; sethi 0x200000,o1; // st o1,[i1]; ret; restore. Independently authored, with the call's delay slot. - let mut words: [u32; 9] = [0x9de3bfa0, 0x9010203e, 0x96100018, 0x953e201f, 0x40000000, - 0x13200000, 0xd2264000, 0x81c7e008, 0x81e80000]; + let mut words: [u32; 9] = [ + 0x9de3bfa0, 0x9010203e, 0x96100018, 0x953e201f, 0x40000000, 0x13200000, 0xd2264000, + 0x81c7e008, 0x81e80000, + ]; if !low_word { words[6] = 0xd0264000; } let bytes: Vec = words.into_iter().flat_map(u32::to_be_bytes).collect(); image.append_section_data(text, &bytes, 4); image.add_symbol(write::Symbol { - name: b"divide_low".to_vec(), value: 0, size: bytes.len() as u64, - kind: SymbolKind::Text, scope: SymbolScope::Linkage, weak: false, - section: write::SymbolSection::Section(text), flags: SymbolFlags::None, + name: b"divide_low".to_vec(), + value: 0, + size: bytes.len() as u64, + kind: SymbolKind::Text, + scope: SymbolScope::Linkage, + weak: false, + section: write::SymbolSection::Section(text), + flags: SymbolFlags::None, }); let helper = image.add_symbol(write::Symbol { - name: b"__divdi3".to_vec(), value: 0, size: 0, kind: SymbolKind::Unknown, - scope: SymbolScope::Dynamic, weak: false, - section: write::SymbolSection::Undefined, flags: SymbolFlags::None, + name: b"__divdi3".to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Unknown, + scope: SymbolScope::Dynamic, + weak: false, + section: write::SymbolSection::Undefined, + flags: SymbolFlags::None, }); - image.add_relocation(text, write::Relocation { - offset: 16, symbol: helper, addend: 0, - flags: RelocationFlags::Elf { r_type: object::elf::R_SPARC_WDISP30 }, - }).unwrap(); + image + .add_relocation( + text, + write::Relocation { + offset: 16, + symbol: helper, + addend: 0, + flags: RelocationFlags::Elf { + r_type: object::elf::R_SPARC_WDISP30, + }, + }, + ) + .unwrap(); image.write().unwrap() } @@ -37,21 +61,28 @@ fn signed_di_quotient_low_word_survives_an_opaque_call() { let root = tempfile::tempdir().unwrap(); let image = root.path().join("divide.o"); std::fs::write(&image, caller(true)).unwrap(); - let compilers: Vec<_> = ["gcc", "clang"].into_iter() - .filter(|cc| Command::new(cc).arg("--version").output().is_ok()).collect(); + let compilers: Vec<_> = ["gcc", "clang"] + .into_iter() + .filter(|cc| Command::new(cc).arg("--version").output().is_ok()) + .collect(); assert!(!compilers.is_empty(), "a C compiler is required"); - for mode in ["off", "on"] { - let (output, error, status) = common::run_kuna(&[ - "decompile", image.to_str().unwrap(), "divide_low", "--json", - "--option", "libproto", mode, - "--assert", "prototype divide_low void divide_low(int divisor,unsigned int *result)", - ]); - assert_eq!(status, 0, "{error}"); - let document: serde_json::Value = serde_json::from_str(&output).unwrap(); - assert!(document["functions"][0]["error"].is_null(), "{output}"); - let body = document["functions"][0]["code"].as_str().unwrap(); - let source = root.path().join(format!("{mode}.c")); - std::fs::write(&source, format!(r#" + let (output, error, status) = common::run_kuna(&[ + "decompile", + image.to_str().unwrap(), + "divide_low", + "--json", + "--assert", + "prototype divide_low void divide_low(int divisor,unsigned int *result)", + ]); + assert_eq!(status, 0, "{error}"); + let document: serde_json::Value = serde_json::from_str(&output).unwrap(); + assert!(document["functions"][0]["error"].is_null(), "{output}"); + let body = document["functions"][0]["code"].as_str().unwrap(); + let source = root.path().join("quotient.c"); + std::fs::write( + &source, + format!( + r#" #include #include long long __divdi3(long long a,long long b) {{ return a/b; }} @@ -65,19 +96,29 @@ int main(void) {{ }} return 0; }} -"#)).unwrap(); - for cc in &compilers { - for opt in ["-O0", "-O2"] { - let executable = root.path().join(format!("{mode}-{cc}-{opt}")); - let built = Command::new(cc).args(["-std=c11", opt, "-Werror=uninitialized"]) - .arg(&source).arg("-o").arg(&executable).output().unwrap(); - if mode == "off" { - assert!(!built.status.success(), "legacy helper call unexpectedly compiled: {body}"); - } else { - assert!(built.status.success(), "{}\n{body}", String::from_utf8_lossy(&built.stderr)); - assert!(Command::new(&executable).status().unwrap().success(), "{cc} {opt}: {body}"); - } - } +"# + ), + ) + .unwrap(); + for cc in &compilers { + for opt in ["-O0", "-O2"] { + let executable = root.path().join(format!("{cc}-{opt}")); + let built = Command::new(cc) + .args(["-std=c11", opt, "-Werror=uninitialized"]) + .arg(&source) + .arg("-o") + .arg(&executable) + .output() + .unwrap(); + assert!( + built.status.success(), + "{}\n{body}", + String::from_utf8_lossy(&built.stderr) + ); + assert!( + Command::new(&executable).status().unwrap().success(), + "{cc} {opt}: {body}" + ); } } } @@ -119,11 +160,142 @@ int main(void) {{ continue; } let executable = root.path().join(cc); - let built = Command::new(cc).args(["-std=c11", "-O2", "-Werror=uninitialized"]) - .arg(&source).arg("-o").arg(&executable).output().unwrap(); - assert!(built.status.success(), "{}\n{body}", String::from_utf8_lossy(&built.stderr)); - assert!(Command::new(&executable).status().unwrap().success(), "{cc}: {body}"); + let built = Command::new(cc) + .args(["-std=c11", "-O2", "-Werror=uninitialized"]) + .arg(&source) + .arg("-o") + .arg(&executable) + .output() + .unwrap(); + assert!( + built.status.success(), + "{}\n{body}", + String::from_utf8_lossy(&built.stderr) + ); + assert!( + Command::new(&executable).status().unwrap().success(), + "{cc}: {body}" + ); checked += 1; } assert!(checked > 0, "a C compiler is required"); } + +#[test] +fn declared_wide_results_survive_caller_return_recovery() { + if Command::new("clang").arg("--version").output().is_err() { + return; + } + let root = tempfile::tempdir().unwrap(); + let fixture = root.path().join("returns.c"); + std::fs::write( + &fixture, + r#" +void g(void); +long long q(long long a,long long b){return a/b;} +long long keep(long long a,long long b){long long r=a/b;g();return r;} +long long guard(long long a,long long b){if(b==0)return -1;return a/b;} +int low(long long a,long long b){return (int)(a/b);} +int high(long long a,long long b){return (int)((a/b)>>32);} +"#, + ) + .unwrap(); + for level in ["-O0", "-O2"] { + let image = root.path().join("returns.o"); + common::process::required_output( + Command::new("clang") + .args(["-target", "sparc-linux-gnu", "-integrated-as", level, "-c"]) + .arg(&fixture) + .arg("-o") + .arg(&image), + ); + for explicit in [false, true] { + let mut bodies = String::new(); + for function in ["q", "keep", "guard", "low", "high"] { + let mut args = vec!["decompile", image.to_str().unwrap(), function, "--json"]; + if explicit { + args.extend([ + "--option", + "libproto", + "off", + "--assert", + "prototype __divdi3 long long __divdi3(long long a,long long b)", + ]); + } + let (output, error, status) = common::run_kuna(&args); + assert_eq!(status, 0, "{error}"); + let document: serde_json::Value = serde_json::from_str(&output).unwrap(); + assert!(document["functions"][0]["error"].is_null(), "{output}"); + bodies.push_str(document["functions"][0]["code"].as_str().unwrap()); + bodies.push('\n'); + } + let source = root.path().join("driver.c"); + std::fs::write(&source, format!(r#" +#include +#include +#include +#define CONCAT44(hi,lo) (((uint64_t)(uint32_t)(hi)<<32)|(uint32_t)(lo)) +static unsigned calls, effects; +long long __divdi3(long long a,long long b) {{ ++calls; return a/b; }} +void g(void) {{ ++effects; }} +{bodies} +#define ARGS(a,b) (uint32_t)((uint64_t)(a)>>32),(uint32_t)(a),(uint32_t)((uint64_t)(b)>>32),(uint32_t)(b) +_Static_assert(sizeof(low(0,0,0,1)) == 4, "the low-word caller returns one word"); +_Static_assert(sizeof(high(0,0,0,1)) == 4, "the high-word caller returns one word"); +int main(void) {{ + const int64_t edges[] = {{0,1,-1,2,-3,INT64_MIN,INT64_MAX,INT64_C(0x123456789abcdef),-INT64_C(0x123456789abcdef)}}; + uint64_t random = UINT64_C(0x79b153c5a7d931e5); + for(unsigned i=0;i<2000;++i) {{ + random ^= random<<13; random ^= random>>7; random ^= random<<17; + int64_t a = i<81 ? edges[i/9] : (int64_t)random; + random ^= random<<13; random ^= random>>7; random ^= random<<17; + int64_t b = i<81 ? edges[i%9] : (int64_t)random; + if(!b || (a==INT64_MIN && b==-1)) continue; + uint64_t expected = (uint64_t)(a/b); + calls = effects = 0; + if(q(ARGS(a,b)) != expected || calls!=1 || effects) return 1; + if(keep(ARGS(a,b)) != expected || calls!=2 || effects!=1) return 2; + if(guard(ARGS(a,b)) != expected || calls!=3 || effects!=1) return 3; + if(low(ARGS(a,b)) != (uint32_t)expected || calls!=4) return 4; + if(high(ARGS(a,b)) != (uint32_t)(expected>>32) || calls!=5) return 5; + if(guard(ARGS(a,0)) != UINT64_MAX || calls!=5 || effects!=1) return 6; + }} + return 0; +}} +"#)).unwrap(); + for cc in ["gcc", "clang"] { + if Command::new(cc).arg("--version").output().is_err() { + continue; + } + for opt in ["-O0", "-O2"] { + let executable = root.path().join(format!("run-{cc}")); + let built = Command::new(cc) + .args([ + "-std=c11", + opt, + "-Werror=uninitialized", + "-fsanitize=undefined", + "-fno-sanitize-recover=all", + ]) + .arg(&source) + .arg("-o") + .arg(&executable) + .output() + .unwrap(); + assert!( + built.status.success(), + "{level} explicit={explicit} {cc} {opt}: {}\n{bodies}", + String::from_utf8_lossy(&built.stderr) + ); + let ran = Command::new(&executable).output().unwrap(); + assert!( + ran.status.success(), + "{level} explicit={explicit} {cc} {opt}: {:?}\n{}\n{bodies}", + ran.status.code(), + String::from_utf8_lossy(&ran.stderr) + ); + } + } + } + } +} diff --git a/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs b/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs index 8cac7e1d98..842bad6525 100644 --- a/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs +++ b/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs @@ -2753,29 +2753,58 @@ impl Heritage { } } - /// Guard RETURN ops when the heritaged range *contains* the output storage, - /// truncating to the recovered output (C++ `Heritage::guardReturnsOverlapping`, - /// `heritage.cc:1610`). Registers the truncated output trial and inserts a - /// SUBPIECE before each RETURN, appending the truncated value as a new input. + /// Guard every model output contained by a wider heritaged range. A locked + /// register-pair call result can cover two separate output entries; keeping + /// only the biggest entry would silently discard the other return word. fn guard_returns_overlapping( &mut self, fd: &mut crate::funcdata::Funcdata, addr: &Address, size: int4, + ) { + self.guard_returns_in_range(fd, addr, size, addr, size, true); + } + + fn guard_returns_in_range( + &mut self, + fd: &mut crate::funcdata::Funcdata, + addr: &Address, + size: int4, + range: &Address, + range_size: int4, + primary: bool, ) { use kuna_num::opcodes::OpCode; use kuna_num::pcoderaw::VarnodeData; let mut vdata = VarnodeData::default(); - if !fd.get_func_proto().get_biggest_contained_output(addr, size, &mut vdata) { + if !fd.get_func_proto().get_biggest_contained_output(range, range_size, &mut vdata) { return; } let vspace = match vdata.space.clone() { Some(s) => s, None => return, }; + let Some(before) = vdata.offset.checked_sub(range.get_offset()) + .and_then(|n| int4::try_from(n).ok()) + else { + return; + }; + let piece_size = vdata.size as int4; + if before > range_size || piece_size <= 0 || piece_size > range_size - before { + return; + } + if before > 0 { + self.guard_returns_in_range(fd, addr, size, range, before, false); + } let trunc_addr = Address::new(Rc::clone(&vspace), vdata.offset); if let Some(active) = fd.get_active_output_mut() { active.register_trial(&trunc_addr, vdata.size as int4); + if !primary { + // The wide range alone is no evidence that the caller returns + // this extra word. Activate it only after a whole-pair proof. + let index = active.get_num_trials() - 1; + active.get_trial_mut(index).mark_inactive(); + } } // offset = least-significant bytes to truncate (endian-adjusted). let mut offset = (vdata.offset.wrapping_sub(addr.get_offset())) as i64 as int4; @@ -2810,6 +2839,10 @@ impl Heritage { .set_active_heritage(); let _ = fd.op_insert_input(op, ret_val, n); } + let after = range_size - before - piece_size; + if after > 0 { + self.guard_returns_in_range(fd, addr, size, &(&trunc_addr + piece_size as i64), after, false); + } } /// (kuna `stackstoreguard`) May this pass guard the frame of `addr`'s diff --git a/decompiler/crates/kuna-decomp/src/p4_calls/coreaction_protos.rs b/decompiler/crates/kuna-decomp/src/p4_calls/coreaction_protos.rs index 18d5a5d8a6..771bd44085 100644 --- a/decompiler/crates/kuna-decomp/src/p4_calls/coreaction_protos.rs +++ b/decompiler/crates/kuna-decomp/src/p4_calls/coreaction_protos.rs @@ -1662,6 +1662,10 @@ impl Action for ActionReturnRecovery { let mut cond_failed = Vec::new(); let return_ops: Vec = data.obank().iter_code(OpCode::CPUI_RETURN).collect(); + if crate::kuna_retcallhalf::awaiting_stack(data, &active, &return_ops) { + data.restore_active_output(active); + return 0; + } for &op in &return_ops { let o = match data.obank().get(op) { Some(o) => o, @@ -1752,11 +1756,21 @@ impl Action for ActionReturnRecovery { crate::p4_calls::kuna_x87emptyreturn::drop_empty(data, &mut active, &return_ops); let own_input = crate::kuna_retcallhalf::accept(data, &mut active, &return_ops); crate::p4_calls::kuna_callwide::retain_pair(data, &mut active, &return_ops); + let whole_pair = crate::kuna_retcallhalf::whole_pair(data, &active, &return_ops); + if let Some(indices) = whole_pair { + for index in indices { + active.get_trial_mut(index).mark_active(); + } + } let manager_rc = data.get_arch().manage.clone(); let _ = data.get_func_proto().derive_output_map(&mut active, &manager_rc); crate::p4_calls::kuna_voidret::whole_or_none(data, &mut active); crate::kuna_retinputhalf::note_moved_back_returns(data, &active, &return_ops, own_input); - let order = crate::kuna_bejoin::join_order(&active, data, &return_ops); + let order = if whole_pair.is_some() && crate::kuna_bejoin::live(data) { + active.join_pair_order() + } else { + crate::kuna_bejoin::join_order(&active, data, &return_ops) + }; data.kuna_set_pairs_first_low(crate::kuna_bejoin::joins_first_low(&active, order)); let return_single = data.get_arch().return_single; for &op in &return_ops { diff --git a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs index df1b3c0f25..26d3cd0659 100644 --- a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs +++ b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs @@ -61,8 +61,9 @@ //! is not changed: `bl g; orr r0,r0,#255` is byte for byte `int f(void) { //! return (int)g() | 255; }` as well as a 64-bit return. Nor is one whose //! second register feeds the first (`asr r1,r0,#2; add r0,r1,r0,lsr #31` is -//! an `int` division by a constant). And a big-endian pair is left alone, -//! because a pair is still joined in little-endian order there. +//! an `int` division by a constant). These heuristics leave high-first pairs +//! alone; [`whole_pair`] separately retains both halves of one declared +//! integer call result, using the model's word order. //! //! # A pair the function names only in part //! @@ -101,6 +102,193 @@ const MAX_DEPTH: u32 = 8; /// How many defining ops the zero test follows. const ZERO_DEPTH: u32 = 8; +/// Preserve pending return reads until scheduled frame SSA can resolve spills. +/// A wide LOAD is not itself proof: it may reload unused incoming arguments. +pub(crate) fn awaiting_stack( + data: &Funcdata, + active: &ParamActive, + returns: &[OpId], +) -> bool { + let Some(stack) = data.get_arch().manage().get_stack_space() else { return false }; + // Spacebase and the simplification pool run after return recovery. Frame + // slots exposed there need the following stack heritage pass to get SSA. + if !data.num_heritage_passes(stack).is_ok_and(|passes| passes <= 1) + || !data.get_func_proto().has_model() || active.get_num_trials() < 2 + { + return false; + } + if !(0..data.num_calls()).any(|i| wide_integer_call(data, data.get_call_specs(i))) { + return false; + } + if !returns.iter().any(|&ret| { + data.obank().get(ret).is_some_and(|op| { + !op.is_dead() && op.get_halt_type() == 0 && (0..active.get_num_trials()).any(|i| { + let value = op.get_in(active.get_trial(i).get_slot()); + value.and_then(|vn| word_source(data, (vn, 0))) + .and_then(|(vn, _)| data.vbank().get(vn)) + .is_some_and(|vn| vn.get_size() == 8 + && vn.get_def().and_then(|id| data.obank().get(id)).is_some_and(|def| def.code() == OpCode::CPUI_LOAD)) + }) + }) + }) { + return false; + } + crate::kuna_bejoin::wide_pair(data.get_func_proto().model(), data.get_arch().manage()) + .is_some_and(|pair| pair.hi.1 == 4 && pair.lo.1 == 4) +} + +/// A complete high-first pair copied from a declared integer call result. +/// Constants on other exits are allowed, but every live exit must define both +/// words, and at least one must return a whole value. +pub(crate) fn whole_pair( + data: &Funcdata, + active: &ParamActive, + returns: &[OpId], +) -> Option<[i32; 2]> { + let proto = data.get_func_proto(); + if proto.is_output_locked() || !proto.has_model() || active.get_num_trials() < 2 { + return None; + } + let pair = crate::kuna_bejoin::wide_pair(proto.model(), data.get_arch().manage())?; + if pair.hi.1 != 4 || pair.lo.1 != 4 { + return None; + } + let mut indices = [0; 2]; + for (slot, (address, size)) in [&pair.hi, &pair.lo].into_iter().enumerate() { + indices[slot] = (0..active.get_num_trials()).find(|&i| { + let trial = active.get_trial(i); + trial.get_address() == address && trial.get_size() == *size && !trial.is_unref() + })?; + } + if (0..active.get_num_trials()) + .any(|i| !indices.contains(&i) && active.get_trial(i).is_active()) + { + return None; + } + let mut wide = false; + for &ret in returns { + let operation = data.obank().get(ret)?; + if operation.is_dead() || operation.get_halt_type() != 0 + || crate::kuna_bejoin::never_reached(data, ret) + { + continue; + } + let hi = operation.get_in(active.get_trial(indices[0]).get_slot())?; + let lo = operation.get_in(active.get_trial(indices[1]).get_slot())?; + wide |= whole_words(data, (hi, 0), (lo, 0), MAX_DEPTH)?; + } + wide.then_some(indices) +} + +/// Follow only representation-preserving moves and exact four-byte slices. +fn word_source(data: &Funcdata, mut word: (VarnodeId, u64)) -> Option<(VarnodeId, u64)> { + // Moves cannot cycle in SSA without a phi, where this walk stops. + // Use the graph's own size as the bound, not an arbitrary copy-chain depth. + for _ in 0..data.vbank().num_varnodes() { + let value = data.vbank().get(word.0)?; + if word.1.checked_add(4)? > value.get_size() as u64 { + return None; + } + let Some(op) = value.get_def().and_then(|id| data.obank().get(id)) else { + return Some(word); + }; + match op.code() { + OpCode::CPUI_COPY => word.0 = op.get_in(0)?, + OpCode::CPUI_INDIRECT if !op.is_indirect_creation() => word.0 = op.get_in(0)?, + OpCode::CPUI_INT_OR if op.get_in(0) == op.get_in(1) => word.0 = op.get_in(0)?, + OpCode::CPUI_INT_ADD | OpCode::CPUI_INT_OR | OpCode::CPUI_INT_XOR => { + let a = op.get_in(0)?; + let b = op.get_in(1)?; + word.0 = if is_zero(data, a, ZERO_DEPTH) { + b + } else if is_zero(data, b, ZERO_DEPTH) { + a + } else { + return Some(word); + }; + } + OpCode::CPUI_SUBPIECE => { + let offset = data.vbank().get(op.get_in(1)?)?; + if !offset.is_constant() { + return None; + } + word = (op.get_in(0)?, word.1.checked_add(offset.get_offset())?); + } + OpCode::CPUI_PIECE => { + let low = op.get_in(1)?; + let low_size = data.vbank().get(low)?.get_size() as u64; + if word.1.checked_add(4)? <= low_size { + word.0 = low; + } else if word.1 >= low_size { + word = (op.get_in(0)?, word.1 - low_size); + } else { + return None; + } + } + _ => return Some(word), + } + } + None +} + +fn whole_words( + data: &Funcdata, + hi: (VarnodeId, u64), + lo: (VarnodeId, u64), + depth: u32, +) -> Option { + if depth == 0 { + return None; + } + let hi = word_source(data, hi)?; + let lo = word_source(data, lo)?; + let h = data.vbank().get(hi.0)?; + let l = data.vbank().get(lo.0)?; + if h.is_constant() && l.is_constant() { + return Some(false); + } + let hop = data.obank().get(h.get_def()?)?; + let lop = data.obank().get(l.get_def()?)?; + if hop.code() == OpCode::CPUI_MULTIEQUAL && lop.code() == OpCode::CPUI_MULTIEQUAL + && hop.get_parent() == lop.get_parent() && hop.num_input() == lop.num_input() + { + let mut wide = false; + for slot in 0..hop.num_input() { + wide |= whole_words(data, (hop.get_in(slot)?, hi.1), (lop.get_in(slot)?, lo.1), depth - 1)?; + } + return Some(wide); + } + if hi.0 != lo.0 || hi.1 != 4 || lo.1 != 0 || h.get_size() != 8 { + return None; + } + let call = match hop.code() { + OpCode::CPUI_CALL | OpCode::CPUI_CALLIND => h.get_def()?, + OpCode::CPUI_INDIRECT if hop.is_indirect_creation() => { + let iop = data.vbank().get(hop.get_in(1)?)?; + OpId::from(slotmap::KeyData::from_ffi(iop.get_offset())) + } + _ => return None, + }; + let spec = data.get_call_specs(data.get_call_specs_index(call)?); + if !wide_integer_call(data, spec) + || hop.code() == OpCode::CPUI_INDIRECT && spec.proto().get_output().get_address() != *h.get_addr() + { + return None; + } + Some(true) +} + +fn wide_integer_call(data: &Funcdata, spec: &crate::fspec::FuncCallSpecs) -> bool { + spec.proto().is_output_locked() + && spec.proto().has_matching_model(data.get_func_proto().model()) + && spec.proto().get_output().get_type().is_some_and(|ty| { + ty.get_size() == 8 && matches!( + ty.get_metatype(), + crate::dtype::type_metatype::TYPE_INT | crate::dtype::type_metatype::TYPE_UINT + ) + }) +} + /// What the first register of a pair holds at one RETURN, when upstream's /// scoring refused it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/docs/spec/04-calls-and-prototypes.md b/docs/spec/04-calls-and-prototypes.md index 844c7041e8..71afb1fe25 100644 --- a/docs/spec/04-calls-and-prototypes.md +++ b/docs/spec/04-calls-and-prototypes.md @@ -2023,6 +2023,26 @@ output sits at the constructed join address (falling back to the first piece if no join can be built); more pieces chain PIECEs over contiguous trials. The (kuna) `returnpair` gate intercepts this join — §4.4. +A wider heritaged register range can contain several separate output entries. +`Heritage::guard_returns_overlapping` keeps the extra entries as inactive +candidates instead of dropping their bytes or treating the range alone as +evidence that the caller returns every word. With a declared wide callee, +return scoring lets frame slots materialized by spacebase simplification +receive stack SSA before finalizing. A wide load alone is not proof, since it +can reload unused incoming arguments. +Before deriving the output map, `kuna_retcallhalf::whole_pair` can retain a +high-first pair when both words at every live return are corresponding slices +of one locked, same-convention eight-byte integer call result. Corresponding +phi inputs are checked together; other exits may return constants, but at least +one must return a whole wide value. The ABI supplies the join order. Explicit +caller outputs and competing output classes are unchanged, and a caller that +returns only a call's low or high word does not acquire its other word. + +The authored `kuna-cli/tests/libgcc_divdi3.rs` round trips cover direct, +saved-across-call and guarded quotient returns at compiler `-O0` and `-O2`, +both with the runtime contract and with an explicit wide prototype while +`libproto` is off. They also check single-word truncations and call effects. + After native return trials are finalized, an otherwise unknown ten-byte scalar result in the full x87 ST0 register receives an extended floating type when the active convention explicitly classifies that exact output storage as floating. From 1f56c353376106f04a3d718a1d63500525714ddc Mon Sep 17 00:00:00 2001 From: Paulo Matias Date: Thu, 22 Oct 2026 22:00:40 -0300 Subject: [PATCH 03/11] [AUTOMATED] Stop pinning broken disabled ARM float output --- docs/baseline-stages.json | 17 +------- tests/stages/kuna-arm-float-return.xml | 58 +++----------------------- tests/stages/kuna-armfloat-preset.xml | 22 ++-------- 3 files changed, 11 insertions(+), 86 deletions(-) diff --git a/docs/baseline-stages.json b/docs/baseline-stages.json index c47e0dd2a0..7562dfa387 100644 --- a/docs/baseline-stages.json +++ b/docs/baseline-stages.json @@ -1,7 +1,7 @@ { "data_footer": [ - 3187, - 3187 + 3174, + 3174 ], "passing": [ "data:64-bit logical shift retains unsigned destination", @@ -1902,7 +1902,6 @@ "data:argclobber-forward #1: the non-clobber path writes the caller's own parameter, so the trailing argument survives both passes", "data:argclobber-forward #2: and caller4 keeps the second parameter that argument reads", "data:argclobber-guards #1: the callee reads rdx before writing it, so the trailing argument survives both passes", - "data:armfloat preset #10: the soft-float image passes its doubles in core registers in both passes", "data:armfloat preset #11: no soft-float function takes a floating parameter in either pass", "data:armfloat preset #12: the preset returns the NaN payload as a float, bit for bit", "data:armfloat preset #13: a float narrowed on one exit and a call's float on the other stays float in both passes", @@ -1912,40 +1911,28 @@ "data:armfloat preset #17: an integer reloaded into s0 from an unoptimized spill keeps its integer parameter in both passes", "data:armfloat preset #18: eight float arguments stored with one vstmia stay parameters in both passes", "data:armfloat preset #19: an int getter that loads a double into d0 on one path keeps its int return in both passes", - "data:armfloat preset #1: off cuts scale's double return to its low word and loses its parameter", "data:armfloat preset #20: an int computed from a double temporary in d0 keeps its int return in both passes", "data:armfloat preset #21: a declared AEABI helper keeps the base standard, so its caller keeps its parameters in both passes", "data:armfloat preset #22: no pass gives the caller of a declared AEABI helper a double parameter", "data:armfloat preset #2: the preset takes and returns scale's double", - "data:armfloat preset #3: off loses mix's double parameter", "data:armfloat preset #4: the preset takes mix's double, float and int", - "data:armfloat preset #5: off reads backfill's double as two integer words", "data:armfloat preset #6: the preset takes backfill's double beside the back-filled float", "data:armfloat preset #7: a float built from its float inputs' bits keeps float parameters in both passes", "data:armfloat preset #8: a NaN payload keeps its bits in every pass", "data:armfloat preset #9: no pass prints the NaN payload as NAN", "data:armfloatreturn #10: on keeps the integer return in r0 while d0 is also written", - "data:armfloatreturn #11: off returns the parsed double of the linked PIE as an integer", "data:armfloatreturn #12: on returns the parsed double of the linked PIE", "data:armfloatreturn #13: an int computed after a call returning a double stays the result in both passes", - "data:armfloatreturn #14: off loses both double parameters of second", "data:armfloatreturn #15: on fills the unused d0 below the returned d1 with one double", "data:armfloatreturn #16: two floats read as the halves of d0 are not one integer parameter in either pass", - "data:armfloatreturn #17: off returns neither path of a3 as a double", "data:armfloatreturn #18: on a3 returns the call result on one path and its own product on the other", "data:armfloatreturn #19: on the computed path of a3 keeps its value", - "data:armfloatreturn #1: a raw image states no calling convention, so both passes return the zero low word of 1.5", "data:armfloatreturn #20: on the constant path of a7 keeps 2.5", "data:armfloatreturn #21: on a6 returns the double both arms of its join assign", - "data:armfloatreturn #22: off a6 returns nothing", "data:armfloatreturn #2: an explicit double prototype on the raw image and the on pass return 1.5", - "data:armfloatreturn #3: off returns the zero low word of the double constant in the object", - "data:armfloatreturn #4: off cuts the double that scale returns to its low word", "data:armfloatreturn #5: on returns the whole double and takes the d0 parameter", "data:armfloatreturn #6: on returns the double constant", - "data:armfloatreturn #7: off loses the parameter of a float return converted from a double", "data:armfloatreturn #8: on keeps the float return in s0 and takes the double parameter", - "data:armfloatreturn #9: off loses the double parameter of an integer function", "data:backing initialization reads the actual ABI parameter", "data:backward BLX #1: retains the call", "data:backward BLX #2: no invented store", diff --git a/tests/stages/kuna-arm-float-return.xml b/tests/stages/kuna-arm-float-return.xml index c7b3455a8d..3381f7fe38 100644 --- a/tests/stages/kuna-arm-float-return.xml +++ b/tests/stages/kuna-arm-float-return.xml @@ -4,10 +4,9 @@ An ARM hard-float function returns a double in d0 and a float in s0 (option armfloatreturn, default off). - The default model lists only the 4-byte s-registers, so a double return - is cut to its low word: fixed() { vmov.f64 d0,#1.5; bx lr } prints - unsigned int fixed(void) { return 0; }, and a double argument read from - d0 is never a parameter. + The default model lists separate 4-byte s-registers. Wide returns need + both return-word recovery and calling-convention evidence. The checks + require correct enabled output, not a particular broken disabled output. The raw image below has no ELF container, so it states no calling convention: the option leaves it alone, and only an explicit prototype @@ -16,8 +15,8 @@ armfloatreturn_armhf.o (clang -mfloat-abi=hard, source beside it) is a relocatable object whose .ARM.attributes says Tag_ABI_VFP_args=1. fmtlf_armhf is a linked PIE whose ELF header carries the hard-float flag. - Pass 1 (option off) reproduces the defect on both; pass 2 (on) returns - the whole double, takes the d0 parameter, narrows a float converted from + Pass 1 (option off) checks shared integer-result controls; pass 2 (on) + returns the whole double, takes the d0 parameter, narrows a float converted from a double to s0, keeps an integer return in r0 while d0 is also written or still holds a callee's double, fills an unused d0 below a used d1 with one double, does not make two floats in s0/s1 one 8-byte parameter, and @@ -27,14 +26,7 @@ -unsigned int raw_fixed\(void\) return 1\.5; -unsigned int fixed\(void\) -return SUB84\(v1 \* 1\.5,0\); float8 scale\(float8 a0\) float8 fixed\(void\) -float4 narrow\(void\) float4 narrow\(float8 a0\) -unsigned int keep\(float8 \*a0\) unsigned int keep\(float8 a0,float8 \*a1\) -return \(int4\)v1; float8 rd\(char \*a0\) int4 bump\(int4 a0\) -void second\(void\) float8 second\(float8 a0,float8 a1\) w2\(void\) -unsigned int a3\(void\) float8 a3\(float8 a0\) return a0 \* 3\.0; return 2\.5; float8 a6\(float8 a0,int4 a1\) -void a6\(int4 a0\) diff --git a/tests/stages/kuna-armfloat-preset.xml b/tests/stages/kuna-armfloat-preset.xml index bd0992069f..b00c459002 100644 --- a/tests/stages/kuna-armfloat-preset.xml +++ b/tests/stages/kuna-armfloat-preset.xml @@ -11,11 +11,10 @@ procedure-call standard (Tag_ABI_VFP_args=1), armfloat_preset_soft.o is the same source for the base standard (-mfloat-abi=soft). - Pass 1 (hard, the preset with both options turned off) reproduces the - defect: scale's double return is cut to its low word and its d0 parameter - is lost, mix loses its double, and backfill's double arrives as two - integer words. Pass 2 (hard, the preset) takes and returns each double - whole, and backfill keeps the back-filled float in s1 beside the double in + Pass 1 (hard, both options off) exercises shared controls without pinning + dropped or truncated double values. Pass 2 (hard, the preset) takes and + returns each double whole, and backfill keeps the back-filled float in s1 + beside the double in d1. In both passes copysign_bits, a float built from the bits of its two float inputs, keeps float parameters, and payload returns a quiet NaN with a payload as its bits rather than as NAN, which compiles to other bits. @@ -57,15 +56,6 @@ mode aggressive option armfloatreturn off option armfloatargs off - lo fu scale - decompile - print C - lo fu mix - decompile - print C - lo fu backfill - decompile - print C lo fu copysign_bits decompile print C @@ -207,16 +197,12 @@ print C quit -unsigned int scale\(void\) double scale\(double a0\) -unsigned int mix\(float a0,int a1\) double mix\(double a0,float a1,int a2\) -float backfill\(float a0,float a1,unsigned int a2,unsigned int a3\) float backfill\(float a0,float a1,double a2\) float copysign_bits\(float a0,float a1\) 0x7fc00123 NAN -void scale\(unsigned int a0,unsigned int a1\) (scale|mix|backfill)\(unsigned int a0 float payload\(void\) float scale_err\(float a0\) From 42d98c336c774dabf7fa0a951e30384d1588e0bb Mon Sep 17 00:00:00 2001 From: Paulo Matias Date: Thu, 22 Oct 2026 22:03:12 -0300 Subject: [PATCH 04/11] [AUTOMATED] Retrigger current-head CI From 668b23ba7502b9405ca106fe8f61e5a66e2b57be Mon Sep 17 00:00:00 2001 From: Paulo Matias Date: Mon, 26 Oct 2026 18:33:46 -0300 Subject: [PATCH 05/11] [AUTOMATED] Preserve wide call returns through literal and partial-word paths --- .../crates/kuna-cli/tests/libgcc_divdi3.rs | 10 +- .../src/p3_dataflow/ruleaction_4.rs | 26 ++- .../src/p3_dataflow/ruleaction_4/tests.rs | 41 +++++ .../kuna-decomp/src/p4_calls/kuna_bejoin.rs | 2 +- .../src/p4_calls/kuna_retcallhalf.rs | 165 +++++++++++++----- docs/spec/04-calls-and-prototypes.md | 35 +++- 6 files changed, 230 insertions(+), 49 deletions(-) diff --git a/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs b/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs index dc2080a6d9..f5cd8b86a7 100644 --- a/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs +++ b/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs @@ -195,6 +195,8 @@ void g(void); long long q(long long a,long long b){return a/b;} long long keep(long long a,long long b){long long r=a/b;g();return r;} long long guard(long long a,long long b){if(b==0)return -1;return a/b;} +long long choose(long long a,long long b,int flag){if(flag)return a/b;return 0x123456789abcdefLL;} +long long mixed(long long a,long long b,int flag){long long r=a/b;if(flag)r=(unsigned int)r;return r;} int low(long long a,long long b){return (int)(a/b);} int high(long long a,long long b){return (int)((a/b)>>32);} "#, @@ -211,7 +213,7 @@ int high(long long a,long long b){return (int)((a/b)>>32);} ); for explicit in [false, true] { let mut bodies = String::new(); - for function in ["q", "keep", "guard", "low", "high"] { + for function in ["q", "keep", "guard", "low", "high", "choose", "mixed"] { let mut args = vec!["decompile", image.to_str().unwrap(), function, "--json"]; if explicit { args.extend([ @@ -242,6 +244,8 @@ void g(void) {{ ++effects; }} #define ARGS(a,b) (uint32_t)((uint64_t)(a)>>32),(uint32_t)(a),(uint32_t)((uint64_t)(b)>>32),(uint32_t)(b) _Static_assert(sizeof(low(0,0,0,1)) == 4, "the low-word caller returns one word"); _Static_assert(sizeof(high(0,0,0,1)) == 4, "the high-word caller returns one word"); +_Static_assert(sizeof(choose(0,0,0,1,0)) == 8, "a nontrivial constant exit stays wide"); +_Static_assert(sizeof(mixed(0,0,0,1,0)) == 8, "one changed word does not discard the other"); int main(void) {{ const int64_t edges[] = {{0,1,-1,2,-3,INT64_MIN,INT64_MAX,INT64_C(0x123456789abcdef),-INT64_C(0x123456789abcdef)}}; uint64_t random = UINT64_C(0x79b153c5a7d931e5); @@ -259,6 +263,10 @@ int main(void) {{ if(low(ARGS(a,b)) != (uint32_t)expected || calls!=4) return 4; if(high(ARGS(a,b)) != (uint32_t)(expected>>32) || calls!=5) return 5; if(guard(ARGS(a,0)) != UINT64_MAX || calls!=5 || effects!=1) return 6; + if(choose(ARGS(a,b),1) != expected || calls!=6 || effects!=1) return 7; + if(choose(ARGS(a,b),0) != UINT64_C(0x123456789abcdef) || calls!=6) return 8; + if(mixed(ARGS(a,b),0) != expected || calls!=7) return 9; + if(mixed(ARGS(a,b),1) != (uint32_t)expected || calls!=8) return 10; }} return 0; }} diff --git a/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4.rs b/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4.rs index b6708b3847..8e5b1fdc13 100644 --- a/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4.rs +++ b/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4.rs @@ -155,15 +155,39 @@ impl RuleLoadVarnode { data: &Funcdata, vn: VarnodeId, spc: &Rc, + depth: u32, ) -> Option<(Rc, uintb)> { if let Some(retspace) = RuleLoadVarnode::correct_spacebase(data, vn, spc) { return Some((retspace, 0)); } + if depth == 8 { + return None; + } let v = data.vbank().get(vn)?; if !v.is_written() { return None; } let op = v.get_def()?; + if code(data, op) == OpCode::CPUI_INT_OR { + let a = in_vn(data, op, 0); + let b = in_vn(data, op, 1); + let (base, constant) = if is_constant(data, b) { + (a, offset_of(data, b)) + } else if is_constant(data, a) { + (b, offset_of(data, a)) + } else { + return None; + }; + // Clang can address a word inside an aligned frame slot as + // (spacebase + offset) | word_offset. It is addition only when + // known-zero bits prove that no set bit can overlap. + if data.vbank().get(base)?.get_nz_mask() & constant != 0 { + return None; + } + let (space, offset) = Self::vn_spacebase(data, base, spc, depth + 1)?; + let offset = space.wrap_offset(offset.wrapping_add(constant)); + return Some((space, offset)); + } if code(data, op) != OpCode::CPUI_INT_ADD { return None; } @@ -205,7 +229,7 @@ impl RuleLoadVarnode { // Check for a literal constant offset into `loadspace`. return Some((loadspace, offset_of(data, offvn))); } - RuleLoadVarnode::vn_spacebase(data, offvn, &loadspace) + RuleLoadVarnode::vn_spacebase(data, offvn, &loadspace, 0) } } diff --git a/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4/tests.rs b/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4/tests.rs index f3cab57b15..72628b8ae4 100644 --- a/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4/tests.rs +++ b/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4/tests.rs @@ -1432,6 +1432,47 @@ fn concatcommute_or_lower() { assert_eq!(fd.vbank().get(cc).unwrap().get_offset(), 0x00ff); } +#[test] +fn loadvarnode_promotes_only_proven_disjoint_or_offsets() { + for (offset, mask, expected) in [ + (0x120, 0x120, Some(0x124)), + (0x124, 0x124, None), + (0x120, u64::MAX, None), + ] { + for commuted in [false, true] { + let mut fd = build_fd(); + let bl = mk_block(&mut fd); + // A constant pseudo-spacebase is the existing global-space form. + // The CLI regression exercises the same rewrite on real frame SSA. + let base = fd.new_constant(8, 0); + fd.vbank_mut().get_mut(base).unwrap().set_spacebase(); + let displacement = fd.new_constant(8, offset); + let (_, address) = mk_def( + &mut fd, bl, OpCode::CPUI_INT_ADD, 8, 0x20, &[base, displacement], + ); + fd.vbank_mut().get_mut(address).unwrap().set_nz_mask(mask); + let word = fd.new_constant(8, 4); + let inputs = if commuted { [word, address] } else { [address, word] }; + let (_, pointer) = mk_def(&mut fd, bl, OpCode::CPUI_INT_OR, 8, 0x30, &inputs); + let space = fd.new_constant(8, ram_space(&fd).get_index() as u64); + let (load, _) = mk_def(&mut fd, bl, OpCode::CPUI_LOAD, 4, 0x40, &[space, pointer]); + let changed = RuleLoadVarnode::new().apply_op(load, &mut fd); + if let Some(expected) = expected { + assert_eq!(changed, 1); + assert_eq!(fd.obank().get(load).unwrap().code(), OpCode::CPUI_COPY); + let input = fd.obank().get(load).unwrap().get_in(0).unwrap(); + let value = fd.vbank().get(input).unwrap(); + assert_eq!(value.get_offset(), expected); + assert_eq!(value.get_size(), 4); + assert_eq!(value.get_space().get_name(), "ram"); + } else { + assert_eq!(changed, 0); + assert_eq!(fd.obank().get(load).unwrap().code(), OpCode::CPUI_LOAD); + } + } + } +} + // --------------------------------------------------------------------------- // Stub-affected rules: assert the documented early-out // --------------------------------------------------------------------------- diff --git a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_bejoin.rs b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_bejoin.rs index af3817896f..cf0af1b02f 100644 --- a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_bejoin.rs +++ b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_bejoin.rs @@ -2265,7 +2265,7 @@ fn ones(size: int4) -> u64 { /// The value of `vn` when it is a constant or integer arithmetic and logic on /// constants alone, within `depth` operations, truncated to its size. -fn literal_value(data: &Funcdata, vn: VarnodeId, depth: u32) -> Option { +pub(crate) fn literal_value(data: &Funcdata, vn: VarnodeId, depth: u32) -> Option { let v = data.vbank().get(vn)?; let size = v.get_size(); let mask = |x: u64, sz: int4| if sz >= 8 { x } else { x & ((1u64 << (8 * sz)) - 1) }; diff --git a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs index 26d3cd0659..ba5438815d 100644 --- a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs +++ b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs @@ -103,43 +103,76 @@ const MAX_DEPTH: u32 = 8; const ZERO_DEPTH: u32 = 8; /// Preserve pending return reads until scheduled frame SSA can resolve spills. -/// A wide LOAD is not itself proof: it may reload unused incoming arguments. +/// A LOAD is not itself proof: it may reload unused incoming arguments. pub(crate) fn awaiting_stack( data: &Funcdata, active: &ParamActive, returns: &[OpId], ) -> bool { let Some(stack) = data.get_arch().manage().get_stack_space() else { return false }; - // Spacebase and the simplification pool run after return recovery. Frame - // slots exposed there need the following stack heritage pass to get SSA. - if !data.num_heritage_passes(stack).is_ok_and(|passes| passes <= 1) - || !data.get_func_proto().has_model() || active.get_num_trials() < 2 - { + let Ok(passes) = data.num_heritage_passes(stack) else { return false }; + if !data.get_func_proto().has_model() || active.get_num_trials() < 2 { return false; } if !(0..data.num_calls()).any(|i| wide_integer_call(data, data.get_call_specs(i))) { return false; } - if !returns.iter().any(|&ret| { + if !crate::kuna_bejoin::wide_pair(data.get_func_proto().model(), data.get_arch().manage()) + .is_some_and(|pair| pair.hi.1 == 4 && pair.lo.1 == 4) + { + return false; + } + returns.iter().any(|&ret| { data.obank().get(ret).is_some_and(|op| { - !op.is_dead() && op.get_halt_type() == 0 && (0..active.get_num_trials()).any(|i| { - let value = op.get_in(active.get_trial(i).get_slot()); - value.and_then(|vn| word_source(data, (vn, 0))) - .and_then(|(vn, _)| data.vbank().get(vn)) - .is_some_and(|vn| vn.get_size() == 8 - && vn.get_def().and_then(|id| data.obank().get(id)).is_some_and(|def| def.code() == OpCode::CPUI_LOAD)) - }) + !op.is_dead() && op.get_halt_type() == 0 + && !crate::kuna_bejoin::never_reached(data, ret) + && (0..active.get_num_trials()).any(|i| { + op.get_in(active.get_trial(i).get_slot()).is_some_and(|vn| { + pending_stack_word(data, (vn, 0), stack.get_index(), passes <= 1, MAX_DEPTH) + }) + }) }) - }) { + }) +} + +/// A frame read which the next simplification pool can materialize for heritage. +/// In particular, a narrow read underneath a phi or extension can survive the +/// first stack SSA pass even after the final eight-byte reload was resolved. +fn pending_stack_word( + data: &Funcdata, + word: (VarnodeId, u64), + stack: i32, + early: bool, + depth: u32, +) -> bool { + if depth == 0 { return false; } - crate::kuna_bejoin::wide_pair(data.get_func_proto().model(), data.get_arch().manage()) - .is_some_and(|pair| pair.hi.1 == 4 && pair.lo.1 == 4) + let Some((vn, offset)) = word_source(data, word) else { return false }; + let Some(value) = data.vbank().get(vn) else { return false }; + let Some(id) = value.get_def() else { return false }; + let Some(op) = data.obank().get(id) else { return false }; + match op.code() { + OpCode::CPUI_LOAD => { + // Before spacebase propagation, preserve the original wide reload. + // Later, wait only for a proven frame access that RuleLoadVarnode + // can rewrite, not an arbitrary load or an extra fixed pass count. + early && value.get_size() == 8 + || crate::ruleaction_4::RuleLoadVarnode::check_spacebase(data, id) + .is_some_and(|(space, _)| space.get_index() == stack) + } + OpCode::CPUI_MULTIEQUAL => (0..op.num_input()).any(|slot| { + op.get_in(slot).is_some_and(|input| { + pending_stack_word(data, (input, offset), stack, early, depth - 1) + }) + }), + _ => false, + } } -/// A complete high-first pair copied from a declared integer call result. -/// Constants on other exits are allowed, but every live exit must define both -/// words, and at least one must return a whole value. +/// A high-first pair with at least one complete declared integer call result. +/// Other paths may replace either word with a literal, including before the +/// simplification pool folds it, but may not introduce unrelated live values. pub(crate) fn whole_pair( data: &Funcdata, active: &ParamActive, @@ -149,6 +182,10 @@ pub(crate) fn whole_pair( if proto.is_output_locked() || !proto.has_model() || active.get_num_trials() < 2 { return None; } + // No word walk can establish a complete declared result without such a call. + if !(0..data.num_calls()).any(|i| wide_integer_call(data, data.get_call_specs(i))) { + return None; + } let pair = crate::kuna_bejoin::wide_pair(proto.model(), data.get_arch().manage())?; if pair.hi.1 != 4 || pair.lo.1 != 4 { return None; @@ -194,6 +231,13 @@ fn word_source(data: &Funcdata, mut word: (VarnodeId, u64)) -> Option<(VarnodeId }; match op.code() { OpCode::CPUI_COPY => word.0 = op.get_in(0)?, + OpCode::CPUI_INT_ZEXT | OpCode::CPUI_INT_SEXT => { + let input = op.get_in(0)?; + if word.1.checked_add(4)? > data.vbank().get(input)?.get_size() as u64 { + return Some(word); + } + word.0 = input; + } OpCode::CPUI_INDIRECT if !op.is_indirect_creation() => word.0 = op.get_in(0)?, OpCode::CPUI_INT_OR if op.get_in(0) == op.get_in(1) => word.0 = op.get_in(0)?, OpCode::CPUI_INT_ADD | OpCode::CPUI_INT_OR | OpCode::CPUI_INT_XOR => { @@ -242,40 +286,83 @@ fn whole_words( } let hi = word_source(data, hi)?; let lo = word_source(data, lo)?; - let h = data.vbank().get(hi.0)?; - let l = data.vbank().get(lo.0)?; - if h.is_constant() && l.is_constant() { + let literal = |word: (VarnodeId, u64)| { + crate::kuna_bejoin::literal_value(data, word.0, ZERO_DEPTH).is_some() + // A word above a zero-extension's input is literal zero even when + // the complete extended value depends on the call's low word. + || data.vbank().get(word.0) + .and_then(|value| value.get_def()) + .and_then(|id| data.obank().get(id)) + .filter(|op| op.code() == OpCode::CPUI_INT_ZEXT) + .and_then(|op| op.get_in(0)) + .and_then(|id| data.vbank().get(id)) + .is_some_and(|input| word.1 >= input.get_size() as u64) + }; + let hi_literal = literal(hi); + let lo_literal = literal(lo); + if hi_literal && lo_literal { return Some(false); } - let hop = data.obank().get(h.get_def()?)?; - let lop = data.obank().get(l.get_def()?)?; - if hop.code() == OpCode::CPUI_MULTIEQUAL && lop.code() == OpCode::CPUI_MULTIEQUAL - && hop.get_parent() == lop.get_parent() && hop.num_input() == lop.num_input() - { - let mut wide = false; - for slot in 0..hop.num_input() { - wide |= whole_words(data, (hop.get_in(slot)?, hi.1), (lop.get_in(slot)?, lo.1), depth - 1)?; + let phi = |vn: VarnodeId| { + data.vbank().get(vn)?.get_def().and_then(|id| data.obank().get(id)) + .filter(|op| op.code() == OpCode::CPUI_MULTIEQUAL) + }; + match (phi(hi.0), phi(lo.0)) { + (Some(hop), Some(lop)) + if hop.get_parent() == lop.get_parent() && hop.num_input() == lop.num_input() => + { + let mut wide = false; + for slot in 0..hop.num_input() { + wide |= whole_words(data, (hop.get_in(slot)?, hi.1), (lop.get_in(slot)?, lo.1), depth - 1)?; + } + return Some(wide); } - return Some(wide); + (Some(hop), None) => { + let mut wide = false; + for slot in 0..hop.num_input() { + wide |= whole_words(data, (hop.get_in(slot)?, hi.1), lo, depth - 1)?; + } + return Some(wide); + } + (None, Some(lop)) => { + let mut wide = false; + for slot in 0..lop.num_input() { + wide |= whole_words(data, hi, (lop.get_in(slot)?, lo.1), depth - 1)?; + } + return Some(wide); + } + _ => {} + } + match (call_word(data, hi, 4), call_word(data, lo, 0)) { + (Some(h), Some(l)) if h == l => Some(true), + (Some(_), None) if lo_literal => Some(false), + (None, Some(_)) if hi_literal => Some(false), + _ => None, } - if hi.0 != lo.0 || hi.1 != 4 || lo.1 != 0 || h.get_size() != 8 { +} + +/// A word at its ABI position in one locked integer call result. +fn call_word(data: &Funcdata, word: (VarnodeId, u64), offset: u64) -> Option { + let value = data.vbank().get(word.0)?; + if word.1 != offset || value.get_size() != 8 { return None; } - let call = match hop.code() { - OpCode::CPUI_CALL | OpCode::CPUI_CALLIND => h.get_def()?, - OpCode::CPUI_INDIRECT if hop.is_indirect_creation() => { - let iop = data.vbank().get(hop.get_in(1)?)?; + let op = data.obank().get(value.get_def()?)?; + let call = match op.code() { + OpCode::CPUI_CALL | OpCode::CPUI_CALLIND => value.get_def()?, + OpCode::CPUI_INDIRECT if op.is_indirect_creation() => { + let iop = data.vbank().get(op.get_in(1)?)?; OpId::from(slotmap::KeyData::from_ffi(iop.get_offset())) } _ => return None, }; let spec = data.get_call_specs(data.get_call_specs_index(call)?); if !wide_integer_call(data, spec) - || hop.code() == OpCode::CPUI_INDIRECT && spec.proto().get_output().get_address() != *h.get_addr() + || op.code() == OpCode::CPUI_INDIRECT && spec.proto().get_output().get_address() != *value.get_addr() { return None; } - Some(true) + Some(word.0) } fn wide_integer_call(data: &Funcdata, spec: &crate::fspec::FuncCallSpecs) -> bool { diff --git a/docs/spec/04-calls-and-prototypes.md b/docs/spec/04-calls-and-prototypes.md index 71afb1fe25..b17e8f680a 100644 --- a/docs/spec/04-calls-and-prototypes.md +++ b/docs/spec/04-calls-and-prototypes.md @@ -2030,16 +2030,37 @@ evidence that the caller returns every word. With a declared wide callee, return scoring lets frame slots materialized by spacebase simplification receive stack SSA before finalizing. A wide load alone is not proof, since it can reload unused incoming arguments. +This also preserves a narrow frame read beneath a phi or extension until +the standard spacebase-load rule can materialize it for the following +heritage pass; resolving only the final eight-byte reload is insufficient. +The spacebase-load consumer also recognizes a constant OR displacement, such +as `(spacebase - 32) | 4`, only when existing nonzero masks prove its set bits +disjoint from the base. Overlapping or unknown bits do not establish an +additive frame offset. Before deriving the output map, `kuna_retcallhalf::whole_pair` can retain a -high-first pair when both words at every live return are corresponding slices -of one locked, same-convention eight-byte integer call result. Corresponding -phi inputs are checked together; other exits may return constants, but at least -one must return a whole wide value. The ABI supplies the join order. Explicit -caller outputs and competing output classes are unchanged, and a caller that -returns only a call's low or high word does not acquire its other word. +high-first pair when at least one path returns corresponding slices of one +locked, same-convention eight-byte integer call result. Corresponding phi +inputs are checked together; a phi in only one word is checked against the +other word on every incoming edge. Other paths may replace either word with +a literal, including constant-only expressions not yet folded by the +simplification pool and the upper zero word of a zero-extension. A lower word +inside an extension keeps its source provenance. Unrelated live values do not +establish a pair, and at least one path must return a whole wide value. The +ABI supplies the join order. Explicit +caller outputs and competing output classes are unchanged. Provenance rejects +single-word truncations that move a different word into the return storage, +including the SPARC register-window controls in the regression. + +This does not prove an unknown caller's C return width from its callee's width. +On a high-first ABI, returning the whole result and returning only its high +word can have identical machine instructions: authored PowerPC32 callers +compiled with sibling-call optimization disabled exhibit exactly that case. +Recovery then retains the wide result; an explicit scalar caller prototype +is needed to state the otherwise indistinguishable narrow contract. The authored `kuna-cli/tests/libgcc_divdi3.rs` round trips cover direct, -saved-across-call and guarded quotient returns at compiler `-O0` and `-O2`, +saved-across-call, guarded, nontrivial-constant and one-word-replaced quotient +returns at compiler `-O0` and `-O2`, both with the runtime contract and with an explicit wide prototype while `libproto` is off. They also check single-word truncations and call effects. From 9ee85cddb47b7551c12c892544acbe0fec966903 Mon Sep 17 00:00:00 2001 From: mahaloz Date: Thu, 29 Oct 2026 00:41:17 +0000 Subject: [PATCH 06/11] [AUTOMATED] Split contained return guards only on high-first pair models guard_returns_overlapping registered every contained output entry on every model. On ARM little-endian the extra entry (s1 under an 8-byte d0, r1 under r0:r1) stays inactive because whole_pair never applies there, and voidret's whole-or-none rule then drops the return entirely: betaflight's hard-float fabs/floor-style routines became `void` with their bodies dead-code eliminated. Register the extra entries only when kuna_bejoin's wide_pair says the model returns its pair high word first; every other model keeps upstream's biggest-contained guard. This restores base output on ARM, so the 13 deleted option-off assertions in kuna-arm-float-return.xml and kuna-armfloat-preset.xml (and their baseline entries) come back unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW --- .../kuna-decomp/src/p3_dataflow/heritage.rs | 21 ++++--- docs/baseline-stages.json | 17 +++++- docs/spec/04-calls-and-prototypes.md | 7 ++- tests/stages/kuna-arm-float-return.xml | 58 +++++++++++++++++-- tests/stages/kuna-armfloat-preset.xml | 22 +++++-- 5 files changed, 105 insertions(+), 20 deletions(-) diff --git a/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs b/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs index e38b56739f..2e02b6fdc1 100644 --- a/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs +++ b/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs @@ -2758,16 +2758,20 @@ impl Heritage { } } - /// Guard every model output contained by a wider heritaged range. A locked - /// register-pair call result can cover two separate output entries; keeping - /// only the biggest entry would silently discard the other return word. + /// Guard the biggest model output contained by a wider heritaged range. On a + /// model whose pair returns the high word first, also guard every other + /// contained output: a locked register-pair call result covers both words, + /// and [`crate::kuna_retcallhalf::whole_pair`] may need the second one. fn guard_returns_overlapping( &mut self, fd: &mut crate::funcdata::Funcdata, addr: &Address, size: int4, ) { - self.guard_returns_in_range(fd, addr, size, addr, size, true); + let split = fd.get_func_proto().has_model() + && crate::kuna_bejoin::wide_pair(fd.get_func_proto().model(), fd.get_arch().manage()) + .is_some(); + self.guard_returns_in_range(fd, addr, size, addr, size, true, split); } fn guard_returns_in_range( @@ -2778,6 +2782,7 @@ impl Heritage { range: &Address, range_size: int4, primary: bool, + split: bool, ) { use kuna_num::opcodes::OpCode; use kuna_num::pcoderaw::VarnodeData; @@ -2798,8 +2803,8 @@ impl Heritage { if before > range_size || piece_size <= 0 || piece_size > range_size - before { return; } - if before > 0 { - self.guard_returns_in_range(fd, addr, size, range, before, false); + if split && before > 0 { + self.guard_returns_in_range(fd, addr, size, range, before, false, true); } let trunc_addr = Address::new(Rc::clone(&vspace), vdata.offset); if let Some(active) = fd.get_active_output_mut() { @@ -2845,8 +2850,8 @@ impl Heritage { let _ = fd.op_insert_input(op, ret_val, n); } let after = range_size - before - piece_size; - if after > 0 { - self.guard_returns_in_range(fd, addr, size, &(&trunc_addr + piece_size as i64), after, false); + if split && after > 0 { + self.guard_returns_in_range(fd, addr, size, &(&trunc_addr + piece_size as i64), after, false, true); } } diff --git a/docs/baseline-stages.json b/docs/baseline-stages.json index 1ecdf9c01a..3a3fe4b714 100644 --- a/docs/baseline-stages.json +++ b/docs/baseline-stages.json @@ -1,7 +1,7 @@ { "data_footer": [ - 3209, - 3209 + 3222, + 3222 ], "passing": [ "data:64-bit logical shift retains unsigned destination", @@ -1928,6 +1928,7 @@ "data:argclobber-forward #1: the non-clobber path writes the caller's own parameter, so the trailing argument survives both passes", "data:argclobber-forward #2: and caller4 keeps the second parameter that argument reads", "data:argclobber-guards #1: the callee reads rdx before writing it, so the trailing argument survives both passes", + "data:armfloat preset #10: the soft-float image passes its doubles in core registers in both passes", "data:armfloat preset #11: no soft-float function takes a floating parameter in either pass", "data:armfloat preset #12: the preset returns the NaN payload as a float, bit for bit", "data:armfloat preset #13: a float narrowed on one exit and a call's float on the other stays float in both passes", @@ -1937,28 +1938,40 @@ "data:armfloat preset #17: an integer reloaded into s0 from an unoptimized spill keeps its integer parameter in both passes", "data:armfloat preset #18: eight float arguments stored with one vstmia stay parameters in both passes", "data:armfloat preset #19: an int getter that loads a double into d0 on one path keeps its int return in both passes", + "data:armfloat preset #1: off cuts scale's double return to its low word and loses its parameter", "data:armfloat preset #20: an int computed from a double temporary in d0 keeps its int return in both passes", "data:armfloat preset #21: a declared AEABI helper keeps the base standard, so its caller keeps its parameters in both passes", "data:armfloat preset #22: no pass gives the caller of a declared AEABI helper a double parameter", "data:armfloat preset #2: the preset takes and returns scale's double", + "data:armfloat preset #3: off loses mix's double parameter", "data:armfloat preset #4: the preset takes mix's double, float and int", + "data:armfloat preset #5: off reads backfill's double as two integer words", "data:armfloat preset #6: the preset takes backfill's double beside the back-filled float", "data:armfloat preset #7: a float built from its float inputs' bits keeps float parameters in both passes", "data:armfloat preset #8: a NaN payload keeps its bits in every pass", "data:armfloat preset #9: no pass prints the NaN payload as NAN", "data:armfloatreturn #10: on keeps the integer return in r0 while d0 is also written", + "data:armfloatreturn #11: off returns the parsed double of the linked PIE as an integer", "data:armfloatreturn #12: on returns the parsed double of the linked PIE", "data:armfloatreturn #13: an int computed after a call returning a double stays the result in both passes", + "data:armfloatreturn #14: off loses both double parameters of second", "data:armfloatreturn #15: on fills the unused d0 below the returned d1 with one double", "data:armfloatreturn #16: two floats read as the halves of d0 are not one integer parameter in either pass", + "data:armfloatreturn #17: off returns neither path of a3 as a double", "data:armfloatreturn #18: on a3 returns the call result on one path and its own product on the other", "data:armfloatreturn #19: on the computed path of a3 keeps its value", + "data:armfloatreturn #1: a raw image states no calling convention, so both passes return the zero low word of 1.5", "data:armfloatreturn #20: on the constant path of a7 keeps 2.5", "data:armfloatreturn #21: on a6 returns the double both arms of its join assign", + "data:armfloatreturn #22: off a6 returns nothing", "data:armfloatreturn #2: an explicit double prototype on the raw image and the on pass return 1.5", + "data:armfloatreturn #3: off returns the zero low word of the double constant in the object", + "data:armfloatreturn #4: off cuts the double that scale returns to its low word", "data:armfloatreturn #5: on returns the whole double and takes the d0 parameter", "data:armfloatreturn #6: on returns the double constant", + "data:armfloatreturn #7: off loses the parameter of a float return converted from a double", "data:armfloatreturn #8: on keeps the float return in s0 and takes the double parameter", + "data:armfloatreturn #9: off loses the double parameter of an integer function", "data:backing initialization reads the actual ABI parameter", "data:backward BLX #1: retains the call", "data:backward BLX #2: no invented store", diff --git a/docs/spec/04-calls-and-prototypes.md b/docs/spec/04-calls-and-prototypes.md index 5ea51b1db0..522ad2b37b 100644 --- a/docs/spec/04-calls-and-prototypes.md +++ b/docs/spec/04-calls-and-prototypes.md @@ -2078,9 +2078,14 @@ if no join can be built); more pieces chain PIECEs over contiguous trials. The (kuna) `returnpair` gate intercepts this join — §4.4. A wider heritaged register range can contain several separate output entries. +On a model whose register pair returns the high word first (`kuna_bejoin`'s +`wide_pair`: SPARC, PowerPC, MIPS and ARM big-endian), `Heritage::guard_returns_overlapping` keeps the extra entries as inactive candidates instead of dropping their bytes or treating the range alone as -evidence that the caller returns every word. With a declared wide callee, +evidence that the caller returns every word. Every other model keeps +upstream's single biggest contained entry: there `whole_pair` never applies, +and an inactive leftover word would make `voidret`'s whole-or-none rule drop +an ARM `d0` or `r0:r1` return the caller still reads. With a declared wide callee, return scoring lets frame slots materialized by spacebase simplification receive stack SSA before finalizing. A wide load alone is not proof, since it can reload unused incoming arguments. diff --git a/tests/stages/kuna-arm-float-return.xml b/tests/stages/kuna-arm-float-return.xml index 3381f7fe38..c7b3455a8d 100644 --- a/tests/stages/kuna-arm-float-return.xml +++ b/tests/stages/kuna-arm-float-return.xml @@ -4,9 +4,10 @@ An ARM hard-float function returns a double in d0 and a float in s0 (option armfloatreturn, default off). - The default model lists separate 4-byte s-registers. Wide returns need - both return-word recovery and calling-convention evidence. The checks - require correct enabled output, not a particular broken disabled output. + The default model lists only the 4-byte s-registers, so a double return + is cut to its low word: fixed() { vmov.f64 d0,#1.5; bx lr } prints + unsigned int fixed(void) { return 0; }, and a double argument read from + d0 is never a parameter. The raw image below has no ELF container, so it states no calling convention: the option leaves it alone, and only an explicit prototype @@ -15,8 +16,8 @@ armfloatreturn_armhf.o (clang -mfloat-abi=hard, source beside it) is a relocatable object whose .ARM.attributes says Tag_ABI_VFP_args=1. fmtlf_armhf is a linked PIE whose ELF header carries the hard-float flag. - Pass 1 (option off) checks shared integer-result controls; pass 2 (on) - returns the whole double, takes the d0 parameter, narrows a float converted from + Pass 1 (option off) reproduces the defect on both; pass 2 (on) returns + the whole double, takes the d0 parameter, narrows a float converted from a double to s0, keeps an integer return in r0 while d0 is also written or still holds a callee's double, fills an unused d0 below a used d1 with one double, does not make two floats in s0/s1 one 8-byte parameter, and @@ -26,7 +27,14 @@ +unsigned int raw_fixed\(void\) return 1\.5; +unsigned int fixed\(void\) +return SUB84\(v1 \* 1\.5,0\); float8 scale\(float8 a0\) float8 fixed\(void\) +float4 narrow\(void\) float4 narrow\(float8 a0\) +unsigned int keep\(float8 \*a0\) unsigned int keep\(float8 a0,float8 \*a1\) +return \(int4\)v1; float8 rd\(char \*a0\) int4 bump\(int4 a0\) +void second\(void\) float8 second\(float8 a0,float8 a1\) w2\(void\) +unsigned int a3\(void\) float8 a3\(float8 a0\) return a0 \* 3\.0; return 2\.5; float8 a6\(float8 a0,int4 a1\) +void a6\(int4 a0\) diff --git a/tests/stages/kuna-armfloat-preset.xml b/tests/stages/kuna-armfloat-preset.xml index b00c459002..bd0992069f 100644 --- a/tests/stages/kuna-armfloat-preset.xml +++ b/tests/stages/kuna-armfloat-preset.xml @@ -11,10 +11,11 @@ procedure-call standard (Tag_ABI_VFP_args=1), armfloat_preset_soft.o is the same source for the base standard (-mfloat-abi=soft). - Pass 1 (hard, both options off) exercises shared controls without pinning - dropped or truncated double values. Pass 2 (hard, the preset) takes and - returns each double whole, and backfill keeps the back-filled float in s1 - beside the double in + Pass 1 (hard, the preset with both options turned off) reproduces the + defect: scale's double return is cut to its low word and its d0 parameter + is lost, mix loses its double, and backfill's double arrives as two + integer words. Pass 2 (hard, the preset) takes and returns each double + whole, and backfill keeps the back-filled float in s1 beside the double in d1. In both passes copysign_bits, a float built from the bits of its two float inputs, keeps float parameters, and payload returns a quiet NaN with a payload as its bits rather than as NAN, which compiles to other bits. @@ -56,6 +57,15 @@ mode aggressive option armfloatreturn off option armfloatargs off + lo fu scale + decompile + print C + lo fu mix + decompile + print C + lo fu backfill + decompile + print C lo fu copysign_bits decompile print C @@ -197,12 +207,16 @@ print C quit +unsigned int scale\(void\) double scale\(double a0\) +unsigned int mix\(float a0,int a1\) double mix\(double a0,float a1,int a2\) +float backfill\(float a0,float a1,unsigned int a2,unsigned int a3\) float backfill\(float a0,float a1,double a2\) float copysign_bits\(float a0,float a1\) 0x7fc00123 NAN +void scale\(unsigned int a0,unsigned int a1\) (scale|mix|backfill)\(unsigned int a0 float payload\(void\) float scale_err\(float a0\) From 20c4eaa496ae2d256ff8919dc170918373f6ef81 Mon Sep 17 00:00:00 2001 From: mahaloz Date: Thu, 29 Oct 2026 01:20:27 +0000 Subject: [PATCH 07/11] [AUTOMATED] Put the SPARC32 __divdi3 contract behind libgccsigs, default off The seeded `long long __divdi3(long long, long long)` fixes a caller that stores only the low word, but a caller that returns arithmetic on the quotient (`a / b + 1`, `-(a / b)`, `f ? a / b : c`) keeps only the high word or returns nothing, where main returns every word: the whole-pair proof accepts bare slices and literals only. Move the seed out of `libproto` into its own analysis pass and option, `libgccsigs`, off by default, so the default output is main's. The generic BE pair-return work stays on. - kuna_libgcc.rs: LibgccSigsPass (id `libgccsigs`), committed only when on. - phases.toml row, dispatch, Architecture flag, analysis_pass_enabled, console live value; catalog counts 339 -> 340 (analysis 89 -> 90), phase_catalog.json, docs/options.md, kuna-catalog.xml #15 and its baseline-stages entry. - libgcc_divdi3 tests pass `--option libgccsigs on`; the store-low-word test also checks that the default equals `off` and keeps main's four-word call. - Spec: the seed is opt-in and why (01), the pair proof's arithmetic limit (04). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW --- .../src/analyzers/protos/kuna_libgcc.rs | 52 +++++++++++-------- .../kuna-analysis/src/analyzers/protos/mod.rs | 3 +- decompiler/crates/kuna-analysis/src/passes.rs | 4 ++ .../crates/kuna-cli/tests/libgcc_divdi3.rs | 46 +++++++++++----- decompiler/crates/kuna-console/src/engine.rs | 1 + .../crates/kuna-console/src/kuna_console.rs | 1 + decompiler/crates/kuna-decomp/phases.toml | 19 +++++++ .../kuna-decomp/src/infra/architecture.rs | 7 +++ .../src/p0_knowledge/kuna_option_dispatch.rs | 1 + .../src/p0_knowledge/kuna_phases/tests.rs | 15 +++--- .../kuna-decomp/tests/catalog_bytecompat.rs | 8 +-- .../tests/fixtures/phase_catalog.json | 1 + docs/baseline-stages.json | 2 +- docs/options.md | 11 ++++ docs/spec/01-program-prep.md | 27 ++++++---- docs/spec/04-calls-and-prototypes.md | 14 ++++- tests/stages/kuna-catalog.xml | 2 +- 17 files changed, 153 insertions(+), 61 deletions(-) diff --git a/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs b/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs index 766ae57c1a..49e6152ac5 100644 --- a/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs +++ b/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs @@ -1,36 +1,46 @@ -//! Compiler-runtime contracts whose integer widths come from machine modes, -//! not the host C data model. Applied under the existing `libproto` option. +//! (kuna `libgccsigs`) Compiler-runtime contracts whose integer widths come from +//! machine modes, not the host C data model. Off by default: a caller that +//! returns arithmetic on the locked eight-byte quotient still loses a word. //! //! `__divdi3` is `di_int (di_int, di_int)`, with `di_int = int64_t`: //! https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/builtins/divdi3.c //! https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/builtins/int_types.h -use std::collections::HashSet; - use object::{Architecture, BinaryFormat, Object}; use super::{seed_named_prototypes, seed_resolved_prototypes, Sig, Ty, L}; -use crate::pass::{AnalysisCtx, AnalysisOutput}; +use crate::pass::{AnalysisCtx, AnalysisOutput, AnalysisPass, Phase}; const SIGNATURES: &[(&str, Sig)] = &[ ("__divdi3", Sig { ret: Ty::Int64, params: &[Ty::Int64, Ty::Int64], vararg: -1 }), ]; -/// Seed the evidenced SPARC32 ELF runtime ABI without retyping other targets. -pub(super) fn seed( - ctx: &AnalysisCtx, - out: &mut AnalysisOutput, - names: &HashSet, - imports: &[(String, u64)], -) { - if ctx.file.format() != BinaryFormat::Elf - || !matches!(ctx.file.architecture(), Architecture::Sparc | Architecture::Sparc32Plus) - || ctx.arch.get_description() != "sparc:BE:32:default:default" - { - return; +pub struct LibgccSigsPass; + +impl AnalysisPass for LibgccSigsPass { + fn phase(&self) -> Phase { + Phase::P1 + } + + fn id(&self) -> &'static str { + "libgccsigs" + } + + /// Seed the evidenced SPARC32 ELF runtime ABI without retyping other targets. + fn run(&self, ctx: &AnalysisCtx) -> AnalysisOutput { + let mut out = AnalysisOutput::default(); + if ctx.file.format() != BinaryFormat::Elf + || !matches!(ctx.file.architecture(), Architecture::Sparc | Architecture::Sparc32Plus) + || ctx.arch.get_description() != "sparc:BE:32:default:default" + { + return out; + } + let (imports, reloc_imports) = super::loaded_import_addrs(ctx.file, ctx.bytes, ctx.image); + let names = super::loaded_function_names(ctx.file, ctx.bytes, &reloc_imports, false); + let types = ctx.arch.types(); + let (_, word_size) = ctx.arch.data_org(); + seed_named_prototypes(&mut out, &names, SIGNATURES, types, word_size, L); + seed_resolved_prototypes(&mut out, &imports, SIGNATURES, types, word_size, L); + out } - let types = ctx.arch.types(); - let (_, word_size) = ctx.arch.data_org(); - seed_named_prototypes(out, names, SIGNATURES, types, word_size, L); - seed_resolved_prototypes(out, imports, SIGNATURES, types, word_size, L); } diff --git a/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs b/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs index 8b528f91cd..ed2ba66eb9 100644 --- a/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs +++ b/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs @@ -38,7 +38,7 @@ use kuna_decomp::fspec::PrototypePieces; use crate::pass::{AnalysisCtx, AnalysisOutput, AnalysisPass, Phase}; -mod kuna_libgcc; +pub mod kuna_libgcc; mod kuna_libcnames; pub mod kuna_libcsigs; pub mod kuna_libctypes; @@ -610,7 +610,6 @@ impl AnalysisPass for LibProtoPass { let (_addr_size, word_size) = ctx.arch.data_org(); seed_named_prototypes(&mut out, &present, LIBC, types, word_size, L); seed_resolved_prototypes(&mut out, &imports, LIBC, types, word_size, L); - kuna_libgcc::seed(ctx, &mut out, &present, &imports); out.c_runtime_imports = crate::loader::pe_iat::c_runtime_import_names(ctx.bytes); out } diff --git a/decompiler/crates/kuna-analysis/src/passes.rs b/decompiler/crates/kuna-analysis/src/passes.rs index 8774d73880..7bbbc3579c 100644 --- a/decompiler/crates/kuna-analysis/src/passes.rs +++ b/decompiler/crates/kuna-analysis/src/passes.rs @@ -88,6 +88,10 @@ pub fn passes_for(compiler: Compiler, format: object::BinaryFormat) -> Vec| { + let mut args = vec![ + "decompile", + image.to_str().unwrap(), + "divide_low", + "--json", + "--assert", + "prototype divide_low void divide_low(int divisor,unsigned int *result)", + ]; + if let Some(value) = option { + args.extend(["--option", "libgccsigs", value]); + } + let (output, error, status) = common::run_kuna(&args); + assert_eq!(status, 0, "{error}"); + let document: serde_json::Value = serde_json::from_str(&output).unwrap(); + assert!(document["functions"][0]["error"].is_null(), "{output}"); + document["functions"][0]["code"].as_str().unwrap().to_string() + }; + let off = decompile(Some("off")); + assert_eq!(decompile(None), off, "libgccsigs is off by default"); + assert!( + !off.contains("CONCAT44") && off.contains("__divdi3(0x3e,0x80000000,"), + "off keeps the inferred four-word call: {off}" + ); + let body = decompile(Some("on")); + assert!(body.contains("__divdi3(0x3e80000000,"), "{body}"); let source = root.path().join("quotient.c"); std::fs::write( &source, @@ -130,6 +145,7 @@ fn an_explicit_scalar_signature_overrides_the_runtime_contract() { std::fs::write(&image, caller(false)).unwrap(); let (output, error, status) = common::run_kuna(&[ "decompile", image.to_str().unwrap(), "divide_low", "--json", + "--option", "libgccsigs", "on", "--assert", "prototype divide_low void divide_low(int value,unsigned int *result)", "--assert", "prototype __divdi3 unsigned int __divdi3(unsigned int a,unsigned int b,unsigned int c,unsigned int d)", ]); @@ -215,7 +231,9 @@ int high(long long a,long long b){return (int)((a/b)>>32);} let mut bodies = String::new(); for function in ["q", "keep", "guard", "low", "high", "choose", "mixed"] { let mut args = vec!["decompile", image.to_str().unwrap(), function, "--json"]; - if explicit { + if !explicit { + args.extend(["--option", "libgccsigs", "on"]); + } else { args.extend([ "--option", "libproto", diff --git a/decompiler/crates/kuna-console/src/engine.rs b/decompiler/crates/kuna-console/src/engine.rs index 64866765c1..4b2da9c7f8 100644 --- a/decompiler/crates/kuna-console/src/engine.rs +++ b/decompiler/crates/kuna-console/src/engine.rs @@ -3009,6 +3009,7 @@ fn analysis_pass_enabled(arch: &Architecture, pass_id: &str) -> bool { // facts are computed at LOAD and COMMITTED only when this gate is on, so // `off` renders exactly what the base table alone renders. "libcsigs" => arch.analysis_libcsigs, + "libgccsigs" => arch.analysis_libgccsigs, // (kuna) The LOAD-TIME format-string resolver — a Listing consumer whose // per-call-site overrides are computed at the deferred commit point and // applied only when the gate asks for the static half (`static` on x86, diff --git a/decompiler/crates/kuna-console/src/kuna_console.rs b/decompiler/crates/kuna-console/src/kuna_console.rs index 3b18802500..2bc65a06f2 100644 --- a/decompiler/crates/kuna-console/src/kuna_console.rs +++ b/decompiler/crates/kuna-console/src/kuna_console.rs @@ -270,6 +270,7 @@ pub fn kuna_live_value(conf: &Architecture, option: &str) -> Option on_off(conf.analysis_noreturn_known), "libproto" => on_off(conf.analysis_libproto), "libcsigs" => on_off(conf.analysis_libcsigs), + "libgccsigs" => on_off(conf.analysis_libgccsigs), // (kuna `libctypes`) Valued `off|opaque|glibc`, so the live token is the // value, not an on/off word. "libctypes" => match (conf.analysis_libctypes, conf.analysis_libctypes_glibc) { diff --git a/decompiler/crates/kuna-decomp/phases.toml b/decompiler/crates/kuna-decomp/phases.toml index 9dcf9ee349..94664ee2c4 100644 --- a/decompiler/crates/kuna-decomp/phases.toml +++ b/decompiler/crates/kuna-decomp/phases.toml @@ -5971,6 +5971,25 @@ change_kind = "correctness-fix" tier = "analysis" symptoms = "a Windows API call renders with an empty argument list|LoadLibraryExW() or CreateFileW() has no arguments but the pushes are visible above it|a PE function declares dozens of stack locals that only ever hold outgoing call arguments|CALL fall-through addresses are assigned to stack variables|a Win32 call's wide string argument renders as a bare 0x... constant|calleearity and varargstackargs both on and a Windows API call is still argumentless" +[[settable]] +option = "libgccsigs" +values = "on|off" +default = "off" +destructive = false +phase = "P1" +subphase = "external-refinement" +strength = "HARD" +rewind = "P1" +issue = "pr-1252-libgcc-divdi3" +summary = "Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract." +use_when = "Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 13 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly." +example = "option libgccsigs on" +source_decompiler = "kuna" +inspiration = "libgcc/compiler-rt `__divdi3` (di_int __divdi3(di_int a, di_int b), di_int = int64_t); Ghidra ApplyDataArchiveAnalyzer" +change_kind = "correctness-fix" +tier = "analysis" +symptoms = "a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns|__divdi3 is called with four 32-bit arguments and only %o0 is read back|*p = v1; after a __divdi3 call where v1 is never written" + [[settable]] option = "declaredlibcproto" values = "on|off" diff --git a/decompiler/crates/kuna-decomp/src/infra/architecture.rs b/decompiler/crates/kuna-decomp/src/infra/architecture.rs index c57792f76a..4694051edd 100644 --- a/decompiler/crates/kuna-decomp/src/infra/architecture.rs +++ b/decompiler/crates/kuna-decomp/src/infra/architecture.rs @@ -1694,6 +1694,9 @@ pub struct Architecture { pub analysis_libproto: bool, /// (kuna) Gate the measured libc signature extension (`libcsigs`); default on. pub analysis_libcsigs: bool, + /// (kuna) Gate the libgcc DI-mode `__divdi3` contract on SPARC32 BE ELF + /// (`libgccsigs`); default off. + pub analysis_libgccsigs: bool, /// (kuna) Gate the named libc/POSIX aggregate types in the prototype tables /// (`libctypes`); default on (`opaque`). Catalog-visible twin of the load-time /// [`crate::kuna_libctypes`] **env var** (the named shells are interned at @@ -3161,6 +3164,7 @@ impl Architecture { analysis_peimportcall: false, analysis_libproto: false, analysis_libcsigs: false, + analysis_libgccsigs: false, analysis_libctypes: false, analysis_libctypes_glibc: false, analysis_win32sigs: false, @@ -3571,6 +3575,9 @@ impl Architecture { self.analysis_libproto = true; // (kuna) DIV-65 measured libc signature extension — default-ON. self.analysis_libcsigs = true; + // (kuna) libgcc `__divdi3` contract -- default-OFF: a caller returning + // arithmetic on the quotient still loses a word. + self.analysis_libgccsigs = false; // (kuna) Named libc/POSIX aggregate pointers in the prototype tables -- // default-ON (`opaque`). Real-ELF analysis tier only, so the XML corpora // cannot observe it; the evidence is the corpus sweep in diff --git a/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_option_dispatch.rs b/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_option_dispatch.rs index 0136fc6214..bbb6662717 100644 --- a/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_option_dispatch.rs +++ b/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_option_dispatch.rs @@ -890,6 +890,7 @@ kuna_options! { self, p1; "peimportcall" => on_off!(analysis_peimportcall, "PE/Mach-O import-slot call binding"), "libproto" => on_off!(analysis_libproto, "Library-prototype analysis pass"), "libcsigs" => on_off!(analysis_libcsigs, "Measured libc signature extension"), + "libgccsigs" => on_off!(analysis_libgccsigs, "libgcc __divdi3 DI-mode contract"), "libctypes" => { use crate::kuna_libctypes::LibcTypesLayout; let layout = match p1.trim().to_ascii_lowercase().as_str() { diff --git a/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_phases/tests.rs b/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_phases/tests.rs index fb2431fd41..7bb3c2097a 100644 --- a/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_phases/tests.rs +++ b/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_phases/tests.rs @@ -38,13 +38,13 @@ fn surface_count_is_120() { } #[test] -fn settable_count_is_339() { - assert_eq!(kuna_num_settables(), 339); - assert_eq!(SETTABLE_TABLE.len(), 339); +fn settable_count_is_340() { + assert_eq!(kuna_num_settables(), 340); + assert_eq!(SETTABLE_TABLE.len(), 340); } #[test] -fn tier_counts_are_120_core_130_transform_89_analysis() { +fn tier_counts_are_120_core_130_transform_90_analysis() { let mut core = 0; let mut transform = 0; let mut analysis = 0; @@ -56,7 +56,7 @@ fn tier_counts_are_120_core_130_transform_89_analysis() { other => panic!("invalid tier {other:?} on {}", s.option), } } - assert_eq!((core, transform, analysis), (120, 130, 89)); + assert_eq!((core, transform, analysis), (120, 130, 90)); } #[test] @@ -318,6 +318,9 @@ fn option_values_live_value_present_for_154() { // with no codegen live reader (read console-side via kuna_live_value), // same as `libcsigs` above. Default-ON (DIV-141). "win32sigs", + // (kuna) The libgcc `__divdi3` contract -- an analysis-pass gate read + // console-side via kuna_live_value, like `libcsigs`. Default-off. + "libgccsigs", "strings", // (kuna) The 2-byte (UTF-16LE) width of the string-literal pass — an // analysis-pass gate read at the commit boundary (console-side via @@ -1109,7 +1112,7 @@ fn emit_catalog_json_static_form_brackets_and_commas() { // +1 for `goreg`; its P1 row sits beside `goabi`. // +1 for `gobool`; its P4 row sits beside `sourcelang`. // +1 for `swiftdemangle`; its P1 row sits beside `sourcelang`. - assert_eq!(json.matches("},\n").count(), 338); + assert_eq!(json.matches("},\n").count(), 339); } #[test] diff --git a/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs b/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs index d33041d029..afba7e9689 100644 --- a/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs +++ b/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs @@ -76,14 +76,14 @@ fn fixture_has_no_current_field() { } #[test] -fn fixture_has_all_339_settables() { +fn fixture_has_all_340_settables() { // One `"option":` per settable row; the authoritative per-option list is // phases.toml settableTable (counts asserted in kuna_phases/tests.rs). - assert_eq!(FIXTURE.matches("\"option\": ").count(), 339); + assert_eq!(FIXTURE.matches("\"option\": ").count(), 340); // Every row carries the tier field appended after change_kind. - assert_eq!(FIXTURE.matches("\"tier\": ").count(), 339); + assert_eq!(FIXTURE.matches("\"tier\": ").count(), 340); // ... and the symptoms array appended after tier (C3). - assert_eq!(FIXTURE.matches("\"symptoms\": ").count(), 339); + assert_eq!(FIXTURE.matches("\"symptoms\": ").count(), 340); } #[test] diff --git a/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json b/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json index 69c1bfafe1..fe5df11bb5 100644 --- a/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json +++ b/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json @@ -216,6 +216,7 @@ {"option": "libcsigs", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-libcsigs", "summary": "Seed the MEASURED extension of the built-in libc prototype table: the 539 further signatures the 28-entry libproto table does not carry (exit, gettext, dcgettext, error, strerror, strdup, strcasecmp, close, open, read, write, stat, getenv, syslog, the FORTIFY __printf_chk/__fprintf_chk/__memcpy_chk family, __errno_location, __stack_chk_fail, the *at family, the SELinux, account, xattr, socket and pthread surfaces, ...). Two mechanical selection rules built it, both over the frozen decbench C corpus and neither one a judgement about which names matter: the first 206 were ranked by PLT call-site frequency (>= 100 call sites, or >= 3 of the 1587 cases where IDA scores a perfect type_match and kuna does not); the 333 that widened it are every name that is an UNDEFINED FUNC symbol in at least 3 of the corpus's 665 dynamically linked binaries and is not already carried. A name in either rule is admitted only if its platform declaration reduces WHOLE to the width-stable Ty vocabulary -- a slot with no honest spelling (off_t, time_t, long long, char) rejects the declaration rather than approximating it, which is why lseek, time, strtoll, mmap, signal and qsort are deliberately absent. Every signature is reduced by script from gcc -aux-info over the platform headers (_GNU_SOURCE + _FORTIFY_SOURCE=2), never written from memory. Applied ONLY to names the image imports and does not itself define, so a program's own function that happens to share a libc spelling is never retyped.", "use_when": "On (default) types the arguments of every common libc call, so a caller whose parameter only flows into one gets a concrete type (char *path) instead of the inferred unsigned long, and the callee's return type is known. Flip OFF when a binary links a private library that reuses libc spellings with different signatures and is NOT statically linked (the pass already skips any name the image defines), or to ablate this table's contribution to a type-recovery difference; off declares exactly the names the 28-entry base table carries and nothing more. It is NOT an ablation of everything a declared prototype does to a call site: the stack placeholder that a call carries until its stack space is heritaged is stripped unconditionally, not by this option, so a base-table name renders with exactly its declared arity in both arms. A declared arity is also an assertion about the CALL SITE, not only about the types: a name this table carries renders with exactly the arguments the signature states, where an unsignatured name renders with whatever argument recovery found. That is the point where a wrong row would show, so the derivation, the rejected set and the whole-corpus diff of the widening live in docs/features/libcwiden/.", "example": "option libcsigs off", "source_decompiler": "kuna", "inspiration": "Ghidra ApplyDataArchiveAnalyzer (.gdt library archives); IDA Pro's til type libraries; measured on decbench C corpus (1587 ida-perfect/kuna-imperfect type_match cases)", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a caller's parameter is unsigned long where it is only ever passed to a libc function that takes a char */int", "a libc call renders its string argument as a bare 0x... constant instead of a literal", "a wrapper around a libc function loses its return value or return type", "__printf_chk / __fprintf_chk arguments are shifted by the fortify flag", "an exit or __stack_chk_fail call shows phantom arguments"]}, {"option": "libctypes", "values": ["off", "opaque", "glibc"], "default": "opaque", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-libctypes", "summary": "Spell the libc/POSIX aggregate pointers in the built-in prototype tables by NAME instead of void *: FILE *, DIR *, stat *, dirent *, passwd *, group *, tm *, option *, timespec *, timeval *, sigaction *, sigset_t *, mbstate_t *, termios *, sockaddr *, pthread_mutex_t *, obstack *, spwd *, utmpx *, utmp *, re_pattern_buffer *, lconv *, statfs *. The Ty vocabulary of those tables is width-stable, so every aggregate pointer in them is a void * today - honest about the width and silent about the pointee, which is the one thing a declaration actually knows. The retarget is enumerated slot by slot (a blanket void *->FILE * would type the va_list of vasprintf/vsnprintf/__vfprintf_chk/verr as a stream), and it brings the stdio names neither shipped table carries: __uflow, fgetc, rewind, freopen, popen, pclose, getdelim, flockfile, funlockfile. A second round of slots was mined from the benchmark corpus's own debug twins - which pointer-to-named-struct variables ground truth holds, and which libc slot each could be reached from - and adds the rest of the stream surface (fseeko, ftello, fread_unlocked, fputc_unlocked, feof_unlocked, fgets_unlocked, setbuf, vfprintf, __getdelim), the record-at-a-time and reentrant halves of the three account databases (getpwent/fgetpwent/putpwent/getpwnam_r and their group and spwd twins), more tm/timespec/termios slots, the regex entry points, localeconv and statfs/fstatfs. Two kinds of slot are left out because naming them splits the caller's frame object: the two-element timespec/timeval arrays of utimensat/futimens/utimes/futimesat, and further sigset_t slots, which a caller hands &sa.sa_mask of a struct sigaction local. Those names are new to both shipped tables, so each supplies an ARITY where there was none as well as a pointee. One table is matched against a name the image DEFINES rather than imports: the five _obstack_* entry points, which are the implementation-reserved half of obstack.h and can only be glibc's or gnulib's copy of the same file. That is what makes obstack reachable at all - gnulib links its copy in and the linker exports the symbols, so a stripped grep, tar or coreutils image carries _obstack_newchunk in .dynsym and nothing else in it says what the first argument addresses; obstack is the widest aggregate in the measured ground truth after FILE. Each named type is interned as a shell carrying its real glibc x86-64 width (FILE 216, stat 144, dirent 280, ...), never width 0 - a zero-width pointee keeps a PTRSUB alive through RulePtrsubUndo and the printer then emits the functional PTRSUB(p,0x28) form - and is left INCOMPLETE so the exported .h declares it opaque rather than as a struct with a width and no members. On an image that carries debug info the platform's own definition is used instead, fields and all: the pass runs after the DWARF importer and adopts an aggregate of the declared width already held under the name (or under glibc's _IO_FILE spelling of FILE) - complete or still being populated - and declines the signature rather than touch a name held by anything else, a different width included. The third value, `glibc`, fills in the nine aggregates whose layout the platform PUBLISHES and an application is meant to read - FILE, stat, timespec, timeval, tm, passwd, group, option, dirent - with their real x86-64 members, so an access renders under its own name (`f->_IO_read_ptr`, `st->st_size`, `opt[i].name`) instead of `f->field_0x8`, and the loaded value takes the FIELD's type rather than whatever the cast said - `st_size` is a signed long, so a wrapper that returns -1 on failure stops printing its sentinel as 0xffffffffffffffff. Every offset and width is measured with offsetof/sizeof against the installed headers, not restated from memory (docs/features/libctypes/glibc.md). Two claims the tables refuse to make: no field is spelled as a pointer to the struct being filled (_IO_FILE::_chain is void * - completing a struct re-keys it into a new Rc, so a self-pointer taken mid-completion would strand the program on two stream types), and no layout is installed unless it is TRUE of the image - x86-64 ELF whose .dynstr names glibc (the libc.so.6 soname or a GLIBC_2.x symbol version). musl, another libc and a statically linked image all keep the opaque shells, which for a static image costs nothing because these tables are matched against imported names only. The WIDTHS are a claim too, and about the caller's frame, not only about offsets inside the shell: a named pointer handed a frame object's address grows that object to the declared width, which is right on x86-64 against glibc (`char v1 [12]` becomes `termios v1` where the program touched twelve bytes of the struct `tcgetattr` fills) and on i386 folded the caller's neighbouring locals into an 8-byte timespec declared as 16 (`v1._12_4_ = a0 + 2`). So nothing at all is named - no slot, no stream symbol, no shell - unless the image is an x86-64 ELF not dynamically linked against another C library (no .dynstr, or one naming glibc); i386, ARM, AArch64, MIPS, PE and Mach-O get the shipped void * tables, byte for byte what `off` gives, and a name declared there by hand does too. A named vote is also held to what the caller does through the pointer. A libc aggregate is often the FIRST member of a larger object (grep's kwset begins with its obstack, an rsyslog instance record with its mutex), whose address is then the one the call reads; where the caller reads that argument past the aggregate's end or before its start, forms an address at or past the end, or steps it by a constant at least as large as the aggregate that is not exactly its size (coreutils wc walks &fstatus[i].st and reads `failed` 8 bytes before it), that call's vote falls back to void * (`kuna_libcfit`) and the declared type decides only the call's cast; type propagation likewise refuses to carry the named pointer onto a value read past the end, so `kwsincr` stops printing every kwset member as an element of an obstack array (`&v1[0x1b].field_0x38`). DIR, sigaction, sigset_t, mbstate_t, termios, sockaddr and pthread_mutex_t stay opaque under `glibc` too: glibc publishes no layout for DIR, and the rest are reserved words no caller reads by name. The same tables also type the stdio STREAMS themselves, which no prototype slot can reach: `stdin`/`stdout`/`stderr` are storage, not an argument, so a function that never calls a typed stdio function sees only an `undefined8` word, and on a shared object the slot has no name at all. Under any enabled value the pass emits a typed DATA symbol for each stream slot, read off the DYNAMIC RELOCATION that binds it - which is also the evidence that the name is the C library's and not a global the program spells `stdout` itself. A COPY relocation names a .bss word the run-time loader fills with libc's own `FILE *stdout`, so that slot is `FILE *` under the stream's own name; a GLOB_DAT on an UNDEFINED symbol names a GOT word holding the stream's ADDRESS, so it is `FILE **` under the minted name `stdout_ptr` - calling that one `stdout` would make the emitted `*stdout_ptr` read as an indirection the program does not perform. Because `stdout_ptr` is kuna's coinage and not the image's, the GLOB_DAT arm declines when either symbol table (.symtab as much as .dynsym) already spells it, rather than printing two addresses under one identifier. Linked ELF only, under the same width gate as the rest of the table, and a spelling that occurs twice in .dynsym is declined outright. The slots point at the same `FILE` the rest of the table uses, so `opaque` gives `stdout->field_0x28` and `glibc` `stdout->_IO_write_ptr`, and an image whose own debug info holds a different `FILE` contributes no stream symbol rather than a contradictory one.", "use_when": "On by default (opaque): a pointer the callee's own libc declaration names carries that name - get_prefix(FILE *f) instead of sub_2f30(long a0) on a stripped -O2 coreutils reader, fopen/fdopen/popen results as FILE *, stat/lstat/fstat filling a stat *, getopt_long reading an option * table, readdir returning a dirent *, localtime a tm *, an obstack handle as obstack * even where gnulib linked the obstack code into the program. Flip OFF to ablate the table: to check whether a pointee name came from this declaration rather than from inference, when a target statically links a private function under a libc spelling that the imports-only restriction did not catch (it would then be typed against libc's aggregate, not its own), or when a decompile-project export has to stay as close to compilable as it gets - the shells are deliberately INCOMPLETE, so the exported .c reads fields out of a type its own .h declares opaque (measured +58 cc errors on an ls export - 51 of them from reading fields out of the incomplete shells, the rest from a type name that collides with a function name of its own in the exported body; the .h itself is 0 errors either way). Off is byte-identical to the shipped void * tables, because with the gate off the pass does not run and not one named shell is interned. No datatest loads a file, so the 675 assertions cannot see this row either way; the stage corpus can, and tests/stages/kuna-libctypes.xml pins it in both arms. The flip's evidence is the corpus sweep in docs/features/libctypes/record.json. Flip to `glibc` for the field NAMES: on an x86-64 ELF against glibc it installs the published layouts of FILE/stat/timespec/timeval/tm/passwd/group/option/dirent, turning `*(unsigned char **)&f->field_0x8` into `f->_IO_read_ptr` and `*(unsigned long *)&st->field_0x30` into `st->st_size`, and giving the loaded value the field's type. Prefer it when reading stdio-heavy or stat-heavy code; stay on `opaque` when a decompile-project export must not declare a type the toolchain's own headers also declare. On a target that is not x86-64 against glibc every value is inert: no slot is named at all, because the widths are not that ABI's. The default also NAMES and TYPES the stream slots: `*(long *)(stdout + 0x28)` becomes `*(long *)&stdout->field_0x28` (`stdout->_IO_write_ptr` under `glibc`), and a shared object's `*(long *)(*dat_3fe0 + 0x28)` becomes `*(long *)&(*stdout_ptr)->field_0x28`. That reach is the function that makes no typed stdio call and the shared object whose stream never had a name - over twelve whole binaries 45 lines move, 38 of them a GOT slot gaining its name, and eight of the twelve are byte-identical because inference had already typed those streams from a typed stdio call in the same function. Its cost is local merging: a slot that takes `FILE *` stops merging with the unrelated values a scalar local had absorbed, which adds a declaration and renumbers the locals after it. Naming a data object stays `datasyms`' call - with that option off the stream slot goes back to `dat_`, since the type rides on the symbol - so ablate the stream half with `datasyms off` and the whole table with `libctypes off`.", "example": "option libctypes off", "source_decompiler": "ghidra", "inspiration": "Ghidra ApplyDataArchiveAnalyzer applying generic_clib_64.gdt, whose FILE */struct stat * slots carry the header's own pointee; signatures reduced from gcc -aux-info over the platform headers (_GNU_SOURCE + _FORTIFY_SOURCE=2), widths from sizeof/_Alignof", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["a function that only ever passes its argument to a libc stdio call still declares it long or unsigned long", "a fopen or fdopen result is a void * that nothing can name", "a stat or lstat call fills an untyped void * buffer", "getopt_long takes a bare (void *)0x... long-option table", "readdir returns a void * whose fields are casts and offsets", "a stream parameter is void * where the ground truth is FILE *", "a FILE * or stat * renders its members as field_0x8 / field_0x30 instead of _IO_read_ptr / st_size", "a value loaded out of a named libc struct keeps the cast's type instead of the field's", "stdin/stdout/stderr render as an untyped word and their fields as raw offsets ((long)stdout + 0x28)", "a shared object's stream slot has no name at all (*(long *)(*dat_3fe0 + 0x28))", "two functions in one binary disagree about what stdout is, one calling it FILE * and the other a long"]}, {"option": "win32sigs", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "repipe-resource-loader-c-retains", "summary": "Seed built-in Win32 API prototypes onto a PE's imported API names, keyed by ENTRY ADDRESS. kuna carried libc signatures (libproto, libcsigs) and not one Windows API signature, so on a PE every LoadLibraryExW/CreateFileW/WriteFile call reached ActionDefaultParams with an empty prototype and its arguments had to be recovered from the call site alone; where that recovery loses -- the ordinary case for an image that writes its outgoing slots well before the call -- the call renders `LoadLibraryExW()` and the argument stores survive as mapped stack locals, along with the return-address push the call itself makes. The table is ~130 WINAPI exports ranked by an import histogram over the 54 PE images of the RE arena corpus (admitted at >= 5 images), plus the resource/loader family (FindResourceW, LoadResource, SizeofResource, EnumResourceTypesW/NamesW) below that bar. Each signature is reduced to the width-stable Ty vocabulary and a declaration with a slot that has no honest spelling (a by-value LARGE_INTEGER, a DWORD64) is rejected rather than approximated. Address is the key because a PE import is TWO FunctionSymbols -- the size-0 IAT slot the engine constant-folds through and the `FF 25` thunk veneer a direct `call` targets -- and the global by-name query used by libproto answers with the slot, which is not the address ActionDefaultParams asks about. Imported names only, so a program's own function sharing a Win32 spelling is never retyped; no CRT (__cdecl) spelling is admitted, because on x86 the PE default model is __stdcall and a locked arity is also the callee's stack pop.", "use_when": "On by default (DIV-141). On, a Windows PE renders `LoadLibraryExW(v1,0,8)` / `WriteFile(h,buf,n,&written,0)` instead of `LoadLibraryExW()` with the pushed values left behind as `v41 = 0x401baf;`-style stack stores. Flip OFF to ablate the table's contribution -- to check whether a Win32 call's arguments came from the parked signature rather than from recovery, or if a target statically links its own function under a Win32 spelling that the imports-only restriction did not catch. Every non-PE target is byte-identical either way.", "example": "option win32sigs off", "source_decompiler": "kuna", "inspiration": "Ghidra ApplyDataArchiveAnalyzer (.gdt library archives); IDA Pro's til type libraries; import histogram over the 54 PE images of the RE arena corpus", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a Windows API call renders with an empty argument list", "LoadLibraryExW() or CreateFileW() has no arguments but the pushes are visible above it", "a PE function declares dozens of stack locals that only ever hold outgoing call arguments", "CALL fall-through addresses are assigned to stack variables", "a Win32 call's wide string argument renders as a bare 0x... constant", "calleearity and varargstackargs both on and a Windows API call is still argumentless"]}, + {"option": "libgccsigs", "values": ["on", "off"], "default": "off", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "pr-1252-libgcc-divdi3", "summary": "Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract.", "use_when": "Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 13 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly.", "example": "option libgccsigs on", "source_decompiler": "kuna", "inspiration": "libgcc/compiler-rt `__divdi3` (di_int __divdi3(di_int a, di_int b), di_int = int64_t); Ghidra ApplyDataArchiveAnalyzer", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns", "__divdi3 is called with four 32-bit arguments and only %o0 is read back", "*p = v1; after a __divdi3 call where v1 is never written"]}, {"option": "declaredlibcproto", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-declaredlibcproto", "summary": "Answer a DECLARED function name out of the built-in libc signature tables, so naming a callee also gives it a prototype. `libproto` and `libcsigs` match a name the IMAGE carries -- its FUNC symbols, its imports -- which is exactly the evidence a stripped, statically linked target does not have. There the name exists only because the operator supplied it (`--define-function 0x8048968=ptrace`, `--assert 'function 0x8048968 ptrace'`), and until now that named the entry and nothing more: the call sites still rendered `ptrace()` with no arguments, because the callee's arity has to come from somewhere and a glibc varargs wrapper (`long ptrace(enum __ptrace_request, ...)`, the rest fetched with `va_arg`) cannot yield it to any amount of body analysis. With this on, a declared name is looked up in both tables and the matching signature is parked on the DECLARED ENTRY ADDRESS -- the key `ActionDefaultParams` reads back per call site, and the only key that survives two symbols sharing a name. The imports-only restriction the `libcsigs` table carries is deliberately lifted here: it exists so a coincidental spelling cannot retype a function the image defines itself, and that is a judgement about evidence, which is different once a human or an agent has identified the entry outright.", "use_when": "On by default: naming a callee is the one thing an RE agent always does, and a name that buys no prototype buys almost nothing. The shape that needs it is a stripped or packed image where argument recovery has nothing to work from -- the tell is a call rendered `f()` with the pushed argument slots visible as raw stores on the lines above it (`*(int *)(esp - 4) = ...` four times, then `v += ptrace();`). Structurally inert unless a run declares a function name (`--define-function START=NAME`, `--assert function`, console `function bounds ... as NAME`), so it is byte-identical over both parity corpora, and inert again unless that name is one of the ~230 the built-in tables carry. Flip OFF when a target defines its own function that merely shares a libc spelling and the declaration is about that function, not the library one -- or to check whether the parked signature, rather than recovery, is what moved a call's arguments. An explicit `--assert prototype` on the same function still wins: it is applied after the declaration.", "example": "kuna decompile ./crackme sub_80483b0 --define-function 0x8048968=ptrace", "source_decompiler": "kuna", "inspiration": "Ghidra ApplyDataArchiveAnalyzer (.gdt archives applied on rename); IDA Pro's til type libraries, which retype a function the moment it is given a library name", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a call to a function I named with --define-function still renders with no arguments", "the argument stores are emitted as raw stack writes on the lines above an argumentless call", "naming a callee changes the spelling of the call and nothing else", "calleearity and varargstackargs both on and a stripped statically linked libc call is still argumentless", "a declared ptrace/read/write/open call loses every argument"]}, {"option": "strings", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "code-data-partition", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-strings", "summary": "Run the string-literal analysis pass: detect NUL-terminated ASCII strings (minimum length 4) and plant a typelocked char[N] data symbol at each. The four-character threshold keeps command and mode literals such as quit, show, bind, fork, and dup2 visible while the separate UTF-16 arm retains Ghidra's length-5 default.", "use_when": "On (default) lays char[N] data at detected strings; off leaves those addresses undefined.", "example": "option strings off", "source_decompiler": "kuna", "inspiration": "Ghidra StringsAnalyzer; pwn.college CSCI 4250 module 5 game servers, where four-character commands and libc operation names rendered as addresses", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["string constants render as raw addresses or unnamed data instead of quoted char[N] literals", "no data symbols at ascii runs in rodata", "four-character commands such as quit, show, bind, or fork render as hexadecimal addresses"]}, {"option": "widestrings", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "code-data-partition", "strength": "HARD", "rewind": "P1", "issue": "re-wide-api-string-args", "summary": "Scan the loaded image at 2-byte (UTF-16LE) width as well as 1-byte, and plant a typelocked wchar2[N] data symbol at each wide literal -- the allCharWidths arm of Ghidra's StringsAnalyzer that kuna's 1-byte port left as a documented seam. Read at 1-byte width a UTF-16LE literal is a ONE-CHARACTER string (`L\"ntdll.dll\"` is 6e 00 74 00 ..., whose first NUL closes the run after `n`), so nothing is marked up there, the constant keeps whatever char * the type lattice gave it, and the printer reads to the same NUL: every wide Windows-API argument rendered as its own first character -- LoadLibraryW(\"n\"), FindWindowW(\"O\",0). The widened matcher is the 1-byte MinLengthCharSequenceMatcher over 2-byte little-endian code units and nothing else: the same printable-ASCII recognizer over each unit's low byte, the same require-NUL-end rule, the same minimum length of 5, over exactly the address set the 1-byte pass scans, on even addresses only. The two widths cannot both claim a run -- a wide unit demands a zero high byte, so five consecutive 1-byte-charset bytes never occur inside a wide run -- and the 1-byte facts are committed first regardless, so the established width owns any address both could reach. With the symbol planted the existing printer path does the rest: the char type's size 2 is what emits the L prefix and reads the bytes two at a time.", "use_when": "On by default. A wide (UTF-16) string argument renders as a one-character literal -- LoadLibraryW(\"n\") where the binary says L\"ntdll.dll\", FindWindowW(\"O\",0) where it says L\"OllyDbg - [CPU]\" -- which is most of what a Windows anti-debugging or anti-VM check contains. `kuna strings --encoding utf16` already reported those literals in full while the decompiled C did not, and the difference between the two surfaces is the tell. Flip OFF to restore markup that is exactly the 1-byte pass's -- e.g. on an image whose 16-bit data tables read as plausible wide text, or to ablate this width's contribution.", "example": "option widestrings off", "source_decompiler": "ghidra-upstream", "inspiration": "Ghidra StringsAnalyzer's allCharWidths setting (the UTF-16 arm of MinLengthCharSequenceMatcher); measured on crackmes.one 65acadf3eef082e477ff5ede (ObfuscationFiesta.exe, PE32+), where sub_140002530's LoadLibraryW / FindWindowW anti-debug checks rendered as one-character strings", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["a wide Windows API argument renders as a one-character string literal", "LoadLibraryW(\"n\") or FindWindowW(\"O\",0) in the emitted C", "kuna strings --encoding utf16 reports a literal the decompiled C truncates to its first character", "a UTF-16 string address carries no data symbol while the ascii ones do", "an anti-debugging check names no DLL or window class"]}, diff --git a/docs/baseline-stages.json b/docs/baseline-stages.json index f1c4ee0b65..54d43ffaa6 100644 --- a/docs/baseline-stages.json +++ b/docs/baseline-stages.json @@ -973,7 +973,7 @@ "data:KUNA-CATALOG #12: scalar ARM inputs are a default-off opt-in", "data:KUNA-CATALOG #13: validated A32 inventory frames are the default", "data:KUNA-CATALOG #14: full ARM frames are explicitly enabled", - "data:KUNA-CATALOG #15: 339 registry rows plus six individual queries", + "data:KUNA-CATALOG #15: 340 registry rows plus six individual queries", "data:KUNA-CATALOG #16: standard C byte-array rendering is explicitly opt-in", "data:KUNA-CATALOG #17: standard C byte-array rendering exposes its live state", "data:KUNA-CATALOG #18: bounded local views are default-on and live", diff --git a/docs/options.md b/docs/options.md index b2b288ec60..72d5893d23 100644 --- a/docs/options.md +++ b/docs/options.md @@ -509,6 +509,9 @@ Three tiers: | CALL fall-through addresses are assigned to stack variables | [`win32sigs`](#win32sigs) | | a Win32 call's wide string argument renders as a bare 0x... constant | [`win32sigs`](#win32sigs) | | calleearity and varargstackargs both on and a Windows API call is still argumentless | [`win32sigs`](#win32sigs) | +| a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns | [`libgccsigs`](#libgccsigs) | +| __divdi3 is called with four 32-bit arguments and only %o0 is read back | [`libgccsigs`](#libgccsigs) | +| *p = v1; after a __divdi3 call where v1 is never written | [`libgccsigs`](#libgccsigs) | | a call to a function I named with --define-function still renders with no arguments | [`declaredlibcproto`](#declaredlibcproto) | | the argument stores are emitted as raw stack writes on the lines above an argumentless call | [`declaredlibcproto`](#declaredlibcproto) | | naming a callee changes the spelling of the call and nothing else | [`declaredlibcproto`](#declaredlibcproto) | @@ -2411,6 +2414,14 @@ Program-prep enablement: what is discovered, decoded, and named before any funct - **Where / provenance:** P1/external-refinement · kuna · correctness-fix · repipe-resource-loader-c-retains - **Example:** `option win32sigs off` +### `libgccsigs` -- on | off, default `off` + +- **Symptoms:** a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns; __divdi3 is called with four 32-bit arguments and only %o0 is read back; *p = v1; after a __divdi3 call where v1 is never written. +- **What it does:** Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract. +- **When to flip:** Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 13 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly. +- **Where / provenance:** P1/external-refinement · kuna · correctness-fix · pr-1252-libgcc-divdi3 +- **Example:** `option libgccsigs on` + ### `declaredlibcproto` -- on | off, default `on` - **Symptoms:** a call to a function I named with --define-function still renders with no arguments; the argument stores are emitted as raw stack writes on the lines above an argumentless call; naming a callee changes the spelling of the call and nothing else; calleearity and varargstackargs both on and a stripped statically linked libc call is still argumentless; a declared ptrace/read/write/open call loses every argument. diff --git a/docs/spec/01-program-prep.md b/docs/spec/01-program-prep.md index 4efe1d28e9..0ee40ff368 100644 --- a/docs/spec/01-program-prep.md +++ b/docs/spec/01-program-prep.md @@ -1849,16 +1849,23 @@ moves. (stale i386 `-O0` outgoing stack slots, the AArch64 `x8` result register), and in an i386 PIC object, whose `GOTOFF` addresses the loader leaves unresolved, a `char *` parameter renders such a garbage address as a string literal. - The same `libproto` gate seeds the reserved compiler-runtime name `__divdi3` - on SPARC32 big-endian ELF under `sparc:BE:32:default:default` - (`decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs (seed)`). - Its DI-mode contract is a signed eight-byte quotient of two signed eight-byte - operands, independent of the target's four-byte C `long`. The normal compiler - model assigns the argument and return register pairs. Thus a caller storing - only `%o1` still stores the low word of the computed quotient instead of reading - an unassigned local. Other names and targets retain their existing inference. - Existing import-address and name-ambiguity rules apply, and DWARF or explicit - prototype assertions retain precedence over the built-in signature. +- **(kuna) libgcc DI-mode contracts** (`libgccsigs`, off by default, + `decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs (LibgccSigsPass)`): + seeds the reserved compiler-runtime name `__divdi3` on SPARC32 big-endian ELF + under `sparc:BE:32:default:default`. Its DI-mode contract is a signed + eight-byte quotient of two signed eight-byte operands, independent of the + target's four-byte C `long`. The normal compiler model assigns the argument + and return register pairs. Thus a caller storing only `%o1` stores the low + word of the computed quotient instead of reading an unassigned local. Other + names and targets retain their existing inference. Existing import-address + and name-ambiguity rules apply, and DWARF or explicit prototype assertions + retain precedence over the built-in signature. The pass's facts are computed + at load and committed only when the option is on. It ships off because the + locked eight-byte result still costs a caller that returns arithmetic on the + quotient: §4's whole-pair proof keeps both return words only when each is a + bare slice of the call result or a literal, so `return a / b + 1;`, + `return -(a / b);` and `return f ? a / b : c;` keep only the high word or + return nothing, where the off output returns every word. - **(kuna) Measured libc signatures** (`libcsigs`, `decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libcsigs.rs (LibcSigsPass)`): the second, larger half of the same table, closing most of the LOSS above. Which diff --git a/docs/spec/04-calls-and-prototypes.md b/docs/spec/04-calls-and-prototypes.md index 522ad2b37b..da88e477cf 100644 --- a/docs/spec/04-calls-and-prototypes.md +++ b/docs/spec/04-calls-and-prototypes.md @@ -2117,11 +2117,21 @@ compiled with sibling-call optimization disabled exhibit exactly that case. Recovery then retains the wide result; an explicit scalar caller prototype is needed to state the otherwise indistinguishable narrow contract. +The pair proof does not follow arithmetic. A caller that returns a value +computed from the call's words (`a / b + 1`, `-(a / b)`, an absolute value, a +shift) or a phi of the call's words and an incoming argument +(`f ? a / b : c`) leaves the second word inactive, so its return keeps only the +high word or none. That is why the SPARC32 `__divdi3` contract (`libgccsigs`, +§1) is off by default: without it the helper is an unknown callee, and such a +caller returns every word in the first-register-low join. With a declared wide +callee from DWARF or `--assert prototype`, the same limit applies. + The authored `kuna-cli/tests/libgcc_divdi3.rs` round trips cover direct, saved-across-call, guarded, nontrivial-constant and one-word-replaced quotient returns at compiler `-O0` and `-O2`, -both with the runtime contract and with an explicit wide prototype while -`libproto` is off. They also check single-word truncations and call effects. +both with `libgccsigs` on and with an explicit wide prototype while +`libproto` is off. They also check single-word truncations, call effects, and +that the default (`libgccsigs` off) leaves a low-word store as before. After native return trials are finalized, an otherwise unknown ten-byte scalar result in the full x87 ST0 register receives an extended floating type when the diff --git a/tests/stages/kuna-catalog.xml b/tests/stages/kuna-catalog.xml index 53b181d265..73258f1090 100644 --- a/tests/stages/kuna-catalog.xml +++ b/tests/stages/kuna-catalog.xml @@ -53,7 +53,7 @@ "option": "armfloatargs", "values": \["on", "off"\], "default": "off", "current": "off" "option": "armframes", "values": \["inventory", "on", "off"\], "default": "inventory", "current": "inventory" "option": "armframes", "values": \["inventory", "on", "off"\], "default": "inventory", "current": "on" -"option": +"option": "option": "globalarrayanchor", "values": \["on", "off"\], "default": "off", "current": "off" "option": "bytearrayc", "values": \["on", "off"\], "default": "off", "current": "off" "option": "bytearrayc", "values": \["on", "off"\], "default": "off", "current": "on" From e1fd36b53655a3d9924cf7af721afb124dd09b9a Mon Sep 17 00:00:00 2001 From: phix33 <122955334+phix33@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:24:35 +1100 Subject: [PATCH 08/11] [AUTOMATED] fix(p2): restart to recover a jump table first found with one entry (#1260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A switch whose index a call writes through a pointer keeps only one case. Its other indexes fall through to the code after the switch. The example below is PPC64: a stack slot is set to 0, a helper writes the switch index through its address, and a five-entry table dispatches on it. Each case returns 40–43, and entries 0 and 4 share a body. ```sh cat > select.s <<'EOF' .abiversion 2 .globl select .type select,@function select: .long 0x7c0802a6,0xf8010010,0xf821ff71,0x39200000,0x38a10070,0x91210070,0x48000091 .long 0x2f83000b,0x419e0074,0x81210070,0x2b890004,0x419d0060,0x7d2a4b78,0x429f0005 .long 0x7d0802a6,0x3d080000,0x39080020,0x794a1764,0x7d2852aa,0x7d294214,0x7d2903a6 .long 0x4e800420,0x00000014,0x0000001c,0x00000024,0x0000002c,0x00000014,0x38600028 .long 0x48000028,0x38600029,0x48000020,0x3860002a,0x48000018,0x3860002b,0x48000010 .long 0x38600063,0x48000008,0x38600001,0x38210090,0xe8010010,0x7c0803a6,0x4e800020 .long 0x90850000,0x38600000,0x4e800020 EOF powerpc-linux-gnu-as -a64 select.s -o select.o kuna decompile select.o select ``` ```c if ((unsigned long)v1[0] <= 4) { switch(*(int *)((unsigned long)v1[0] * 4 + 0x400058) + 0x400058L) { case 0x40006c: return 0x28; } } return 99; ``` Ghidra prints `case 0: case 4: … case 1: … case 2: … case 3:` for the same code. ## Fix - During flow, the table is recovered on a partial copy of the function. That copy doesn't yet know the call takes the slot's address, so it reads back the stored 0 and builds a one-entry table. The full analysis later finds five entries, but the switch has already been built with one out-edge. - Port upstream's multistage restart. When the flow-time table has one address and the late model has more, `matchModel` records a multistage hint for the jump and restarts the function. The next flow re-queues the table through `checkForMultistage`/`recoverMultistage`. - The second stage runs on the same kind of partial copy, so it can fold the index again. The hint therefore also carries the late model's targets, and a second stage that again finds one address uses them. - A jump restarts at most once, and only when every value in the late model's range passes the guards on its path and every target is mapped and within the `0xffff` reach `sanityCheck` allows. A model wider than its guards would otherwise turn rows a lower bound excludes into cases. - When the restart budget runs out, the last pass (which still labels its table) is printed with an `Exceeded maximum restarts with more pending` warning. ## Tests - `kuna-cli` `ppc64_delayed_constant_switch`, each big- and little-endian: - the function above keeps `case 0: case 4:`, `case 1:` to `case 3:` and `default:`; - a `5 <= i && i <= 9` guard over a table whose rows 0–4 also hold mapped targets keeps the lower bound, and prints none of those rows as cases; - a chain of nine such switches keeps a structured body under the restartlimit. The first fails without the fix. The other two fail if the restart skips the guard check or re-follows flow at the limit. - No datatest or stage expectation moved, and a sweep of 478 ELF fixtures changes no function. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW --- .../crates/kuna-cli/tests/integration.rs | 3 + .../tests/ppc64_delayed_constant_switch.rs | 153 ++++++++++++++++++ .../kuna-decomp/src/infra/decompile_drive.rs | 5 +- .../kuna-decomp/src/p0_knowledge/overrides.rs | 15 ++ .../crates/kuna-decomp/src/p2_lift/flow.rs | 33 +++- .../kuna-decomp/src/p2_lift/jumptable.rs | 114 ++++++++++--- .../src/substrate/funcdata_block.rs | 7 +- docs/spec/00-overview.md | 17 +- docs/spec/02-lift-and-flow.md | 34 +++- 9 files changed, 333 insertions(+), 48 deletions(-) create mode 100644 decompiler/crates/kuna-cli/tests/ppc64_delayed_constant_switch.rs diff --git a/decompiler/crates/kuna-cli/tests/integration.rs b/decompiler/crates/kuna-cli/tests/integration.rs index 94e5873580..7af43c4739 100644 --- a/decompiler/crates/kuna-cli/tests/integration.rs +++ b/decompiler/crates/kuna-cli/tests/integration.rs @@ -292,6 +292,9 @@ mod passthrough_own_return; #[path = "pointer_table_strings.rs"] mod pointer_table_strings; +#[path = "ppc64_delayed_constant_switch.rs"] +mod ppc64_delayed_constant_switch; + #[path = "powerpc_isa_selection.rs"] mod powerpc_isa_selection; diff --git a/decompiler/crates/kuna-cli/tests/ppc64_delayed_constant_switch.rs b/decompiler/crates/kuna-cli/tests/ppc64_delayed_constant_switch.rs new file mode 100644 index 0000000000..5d43edf968 --- /dev/null +++ b/decompiler/crates/kuna-cli/tests/ppc64_delayed_constant_switch.rs @@ -0,0 +1,153 @@ +//! A PPC64 switch whose index a call writes keeps every case of its table. + +use crate::common; + +use object::write::{Object, Symbol, SymbolSection}; +use object::{ + Architecture, BinaryFormat, Endianness, FileFlags, SectionKind, SymbolFlags, SymbolKind, + SymbolScope, +}; +use std::process::Command; + +/// `select` stores 0 to a stack slot, passes its address to `get_state`, which +/// writes it, then reads the slot back, guards it and dispatches through a +/// five-entry relative table (`base + sext(table[i])`, entries 0 and 4 sharing +/// a target). The base is `bcl`/`mflr` plus `addis`/`addi`, a constant expression +/// several ops deep. Each case returns its own constant. +const SELECT: [u32; 45] = [ + 0x7c0802a6, 0xf8010010, 0xf821ff71, 0x39200000, 0x38a10070, 0x91210070, 0x48000091, + 0x2f83000b, 0x419e0074, 0x81210070, 0x2b890004, 0x419d0060, 0x7d2a4b78, 0x429f0005, + 0x7d0802a6, 0x3d080000, 0x39080020, 0x794a1764, 0x7d2852aa, 0x7d294214, 0x7d2903a6, + 0x4e800420, 0x00000014, 0x0000001c, 0x00000024, 0x0000002c, 0x00000014, 0x38600028, + 0x48000028, 0x38600029, 0x48000020, 0x3860002a, 0x48000018, 0x3860002b, 0x48000010, + 0x38600063, 0x48000008, 0x38600001, 0x38210090, 0xe8010010, 0x7c0803a6, 0x4e800020, + 0x90850000, 0x38600000, 0x4e800020, +]; + +/// [`SELECT`] with the slot stored as 6 and the guard `5 <= i && i <= 9` over a +/// ten-entry table whose rows 0 to 4 also hold mapped targets (returning 40 to +/// 44). Those rows never reach the switch, so they must not become cases. +const LOWER_BOUND: [u32; 64] = [ + 0x7c0802a6, 0xf8010010, 0xf821ff71, 0x39200006, 0x38a10070, 0x91210070, 0x480000dd, + 0x2f83000b, 0x419e00c0, 0x81210070, 0x2b890005, 0x419c00ac, 0x2b890009, 0x419d00a4, + 0x7d2a4b78, 0x429f0005, 0x7d0802a6, 0x3d080000, 0x39080020, 0x794a1764, 0x7d2852aa, + 0x7d294214, 0x7d2903a6, 0x4e800420, 0x28, 0x30, 0x38, 0x40, 0x48, 0x50, 0x58, 0x60, 0x68, + 0x70, 0x38600028, 0x48000058, 0x38600029, 0x48000050, 0x3860002a, 0x48000048, 0x3860002b, + 0x48000040, 0x3860002c, 0x48000038, 0x3860002d, 0x48000030, 0x3860002e, 0x48000028, + 0x3860002f, 0x48000020, 0x38600030, 0x48000018, 0x38600031, 0x48000010, 0x38600063, + 0x48000008, 0x38600001, 0x38210090, 0xe8010010, 0x7c0803a6, 0x4e800020, 0x90850000, + 0x38600000, 0x4e800020, +]; + +/// `n` copies of [`SELECT`]'s dispatch in a chain: case 1 of stage `k` runs +/// stage `k + 1`, so each restart reveals one more switch. Stage `k`'s cases +/// return `100 + 10k`, `+ 2`, `+ 3`, and its default `+ 9`. +fn nested(n: usize) -> Vec { + const STAGE: usize = 31; + let out = 3 + n * STAGE + 1; + let get_state = out + 4; + let branch = |op: u32, from: usize, to: usize| op | ((to as i64 - from as i64) as u32 * 4 & 0x03ff_fffc); + let mut code = vec![0x7c0802a6, 0xf8010010, 0xf821ff71]; + for k in 0..n { + let s = code.len(); + let value = |v: usize| 0x38600000 | (100 + 10 * k + v) as u32; + code.extend([0x39200000, 0x38a10070, 0x91210070, branch(0x48000001, s + 3, get_state)]); + code.extend([0x81210070, 0x2b890004, 0x419d0058, 0x7d2a4b78, 0x429f0005, 0x7d0802a6]); + code.extend([0x3d080000, 0x39080020, 0x794a1764, 0x7d2852aa, 0x7d294214, 0x7d2903a6]); + code.extend([0x4e800420, 0x14, 0x34, 0x1c, 0x24, 0x14]); + for (slot, v) in [(22, 0), (24, 2), (26, 3), (28, 9)] { + code.extend([value(v), branch(0x48000000, s + slot + 1, out)]); + } + if k + 1 < n { + code.push(branch(0x48000000, s + 30, s + STAGE)); + } else { + code.extend([0x38600007, branch(0x48000000, s + 31, out)]); + } + } + assert_eq!(code.len(), out); + code.extend([0x38210090, 0xe8010010, 0x7c0803a6, 0x4e800020]); + code.extend([0x90850000, 0x38600000, 0x4e800020]); + code +} + +fn image(endian: Endianness, words: &[u32]) -> Vec { + let mut object = Object::new(BinaryFormat::Elf, Architecture::PowerPc64, endian); + object.flags = FileFlags::Elf { os_abi: 0, abi_version: 0, e_flags: 2 }; + let code: Vec = words + .iter() + .flat_map(|&n| match endian { + Endianness::Big => n.to_be_bytes(), + Endianness::Little => n.to_le_bytes(), + }) + .collect(); + let text = object.add_section(Vec::new(), b".text".to_vec(), SectionKind::Text); + object.append_section_data(text, &code, 4); + let helper = code.len() as u64 - 12; + for (name, value, size) in [("select", 0, helper), ("get_state", helper, 12)] { + object.add_symbol(Symbol { + name: name.as_bytes().to_vec(), + value, + size, + kind: SymbolKind::Text, + scope: SymbolScope::Linkage, + weak: false, + section: SymbolSection::Section(text), + flags: SymbolFlags::None, + }); + } + object.write().unwrap() +} + +/// Decompile `select` from `words` in both byte orders, flattening whitespace. +fn decompile_both(name: &str, words: &[u32]) -> Vec<(Endianness, String, String)> { + let path = common::scratch_file(name, "o"); + let mut results = Vec::new(); + for endian in [Endianness::Big, Endianness::Little] { + std::fs::write(&path, image(endian, words)).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_kuna")) + .args(["decompile", path.to_str().unwrap(), "select"]) + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + let code = String::from_utf8(output.stdout).unwrap(); + let flat = code.split_whitespace().collect::>().join(" "); + results.push((endian, code, flat)); + } + std::fs::remove_file(path).unwrap(); + results +} + +#[test] +fn a_call_written_switch_index_keeps_every_table_case() { + for (endian, code, flat) in decompile_both("ppc64-delayed-constant-switch", &SELECT) { + assert!(!code.contains("does not reproduce the recovered table"), "{endian:?}: {code}"); + for body in [ + "case 0: case 4: return 0x28;", + "case 1: return 0x29;", + "case 2: return 0x2a;", + "case 3: return 0x2b;", + "default: return 99;", + ] { + assert!(flat.contains(body), "{endian:?}: lost `{body}`: {code}"); + } + } +} + +#[test] +fn rows_a_lower_bound_guard_excludes_do_not_become_cases() { + for (endian, code, flat) in decompile_both("ppc64-delayed-lower-bound", &LOWER_BOUND) { + assert!(flat.contains("5 <="), "{endian:?}: dropped the lower bound: {code}"); + for value in 0x28..=0x2c { + assert!(!flat.contains(&format!(": return {value:#x};")), "{endian:?}: row of {value:#x} became a case: {code}"); + } + } +} + +#[test] +fn an_exhausted_restart_budget_keeps_the_analyzed_body() { + for (endian, code, _) in decompile_both("ppc64-delayed-nested-switch", &nested(9)) { + assert!(!code.contains("structuring declined"), "{endian:?}: lost the body: {code}"); + assert!(code.contains("Exceeded maximum restarts with more pending"), "{endian:?}: {code}"); + assert!(code.matches("switch(").count() >= 8, "{endian:?}: {code}"); + } +} diff --git a/decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs b/decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs index 8d3f3f6a05..f1a4fbe832 100644 --- a/decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs +++ b/decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs @@ -1072,7 +1072,10 @@ fn run_pipeline(arch: &mut Architecture, fd: &mut Funcdata) -> KunaResult return Ok(total); } if reflows >= MAX_REFLOW { - // Exceeded the cross-flow restart budget; keep the last analyzed IR. + // Exceeded the cross-flow restart budget. The last pass's analysis + // is complete; re-following now would clear it and leave nothing + // analyzed to print, so keep it and say more restarts were pending. + fd.warning_header("Exceeded maximum restarts with more pending"); fd.set_restart_pending(false); drain_pipeline_comments(arch, fd); return Ok(total); diff --git a/decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs b/decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs index 95301733cd..46955a6933 100644 --- a/decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs +++ b/decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs @@ -239,6 +239,9 @@ pub struct Override { protoover: BTreeMap>, /// Addresses of indirect jumps that need multistage recovery. multistagejump: Vec
, + /// (kuna) The targets the late model recovered for a multistage jump, used + /// when the second flow-time stage still recovers a single address. + multistagetargets: BTreeMap>, /// Override the CALL <-> BRANCH interpretation of a branch instruction. flowoverride: BTreeMap, /// `/GS` checker call sites recognized after SSA and replayed on restart. @@ -263,6 +266,7 @@ impl Override { self.indirectover.clear(); self.protoover.clear(); self.multistagejump.clear(); + self.multistagetargets.clear(); self.flowoverride.clear(); self.msvc_cookie_calls.clear(); self.format_calls.clear(); @@ -344,6 +348,17 @@ impl Override { self.multistagejump.push(addr); } + /// (kuna) Record the targets a late model recovered for the multistage jump + /// at `addr`. + pub fn insert_multistage_targets(&mut self, addr: Address, targets: Vec
) { + self.multistagetargets.insert(addr, targets); + } + + /// (kuna) The targets recorded for the multistage jump at `addr`. + pub fn multistage_targets(&self, addr: &Address) -> Option<&[Address]> { + self.multistagetargets.get(addr).map(Vec::as_slice) + } + /// Mark a branch instruction with a different flow type /// (C++ `Override::insertFlowOverride`). pub fn insert_flow_override(&mut self, addr: Address, type_: uint4) { diff --git a/decompiler/crates/kuna-decomp/src/p2_lift/flow.rs b/decompiler/crates/kuna-decomp/src/p2_lift/flow.rs index eb74dd5fb3..a633e5df1f 100644 --- a/decompiler/crates/kuna-decomp/src/p2_lift/flow.rs +++ b/decompiler/crates/kuna-decomp/src/p2_lift/flow.rs @@ -2578,10 +2578,8 @@ truncating the fall-through here" /// caller binds it to the real [`Architecture`]'s action root). For each /// recovered table, `newAddress` re-fills as much flow as possible. /// - /// STUB(W4): `checkContainedCall` / `checkMultistageJumptables` (the PIC / - /// multistage outer loop) need the override table + FuncCallSpecs; the single - /// recovery pass over the current `tablelist` is the load-bearing part for the - /// corpus and runs fully. + /// STUB(W4): `checkContainedCall` (the PIC outer loop) needs FuncCallSpecs; + /// `checkMultistageJumptables` re-queues a table a multistage hint marks. /// /// The outer `do/while` drains `injectlist` on every round (`flow.cc:819`), so a /// registered `` is applied to table-discovered flow exactly as it @@ -2611,10 +2609,8 @@ truncating the fall-through here" } } } - // STUB(W4): checkContainedCall + checkMultistageJumptables (outer - // do/while) — the multistage/inline restart that could re-populate - // tablelist is the W4 override table; the single pass suffices for the - // corpus switches. + // STUB(W4): checkContainedCall (PIC constructions) is not ported. + self.check_multistage_jumptables()?; if self.has_inject() { self.inject_pcode()?; } @@ -2625,6 +2621,27 @@ truncating the fall-through here" Ok(()) } + /// Queue each jump-table that a multistage hint asks to recover again, + /// now that flow has filled in more of the function (C++ + /// `FlowInfo::checkMultistageJumptables`, `flow.cc:1393`). + fn check_multistage_jumptables(&mut self) -> KunaResult<()> { + for i in 0..self.data.num_jump_tables() { + let mut jt = std::mem::replace( + self.data.get_jump_table_mut(i), + crate::jumptable::JumpTable::new(Address::default()), + ); + let res = jt.check_for_multistage(&self.data); + let indop = jt.get_indirect_op(); + *self.data.get_jump_table_mut(i) = jt; + if res? { + if let Some(op) = indop { + self.tablelist.push(op); + } + } + } + Ok(()) + } + /// Generate basic blocks from the raw control-flow (C++ `generateBlocks`, /// `flow.cc:826`). /// diff --git a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs index d9d90c2bd5..e65f11b1b1 100644 --- a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs +++ b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs @@ -5973,7 +5973,7 @@ impl JumpTable { return Ok(()); } if jbasic.rows_rejected { - self.drop_model_for_rows(fd); + self.drop_model_for_rows(fd, true); return Ok(()); } // Try model 2 (default-path). @@ -6165,6 +6165,16 @@ impl JumpTable { Ok(()) } + /// (kuna) A second flow-time stage that again recovered a single address + /// adopts the targets the late model recorded with the multistage hint, so + /// flow follows every case of the table. + pub fn kuna_adopt_multistage_targets(&mut self, targets: Vec
) { + if self.addresstable.len() == 1 && targets.len() > 1 { + self.addresstable = targets; + self.row_loads = None; + } + } + /// Set manual override information on \b this jump-table /// (C++ `JumpTable::setOverride`, `jumptable.cc:2616`). /// @@ -6186,9 +6196,10 @@ impl JumpTable { /// Try to match the JumpTable model to the existing function /// (C++ `JumpTable::matchModel`, `jumptable.cc:2833`). /// - /// The table keeps its flow-recovered addresses; the multistage restart - /// upstream takes on a size mismatch is replaced by a row check that - /// decides which values may label them ([`Self::choose_label_values`]). + /// A one-address flow-time table whose current model has more entries + /// requests upstream's multistage restart ([`Self::request_multistage`]). + /// Any other size mismatch keeps the flow-recovered addresses, and a row + /// check decides which values may label them ([`Self::choose_label_values`]). pub fn match_model(&mut self, fd: &mut Funcdata) -> KunaResult<()> { if !self.is_recovered() { return Err(KunaError::lowlevel( @@ -6212,20 +6223,72 @@ impl JumpTable { .and_then(|m| LabelRows::new(m, &self.addresstable, self.row_loads.as_ref())) .map(|rows| Rc::new(rows.with_default(self.default_addresses()))); self.recover_model(fd, rows.clone())?; // Create a current instance of the model + // (kuna) Unlike upstream, keep labelling this pass: when the drive's + // restart budget is spent, this pass is the one printed, under its own + // restart warning rather than the model-drop one. + let restarting = self.request_multistage(fd); if let Some(rows) = rows { - self.choose_label_values(fd, &rows); + self.choose_label_values(fd, &rows, !restarting); } Ok(()) } + /// The multistage restart of C++ `JumpTable::matchModel` (`jumptable.cc:2709`): + /// a flow-time table of one address whose current model has more entries was + /// not fully recovered during flow, so record the jump for a second recovery + /// stage and request a restart. + /// + /// (kuna) The current model's own targets are recorded with the hint, since + /// the second flow-time stage repeats the first one's view of the function + /// and can recover the same single address. A jump is restarted at most once, + /// and only when every value in the model's range passes the guards on its + /// path (a range wider than the guards, as `switchguardbound` can recover, + /// would turn guarded-out rows into cases) and every target is mapped and + /// within the `0xffff` reach that `sanity_check` allows an intraprocedural + /// table. + fn request_multistage(&self, fd: &mut Funcdata) -> bool { + let Some(model) = self.jmodel.as_ref() else { return false }; + if self.addresstable.len() != 1 + || model.get_table_size() <= 1 + || fd.get_override().query_multistage_jumptable(&self.opaddress) + { + return false; + } + let Some(indirect) = self.indirect else { return false }; + let dispatched = model + .as_any() + .downcast_ref::() + .and_then(|basic| Some(basic.values_pass_guards(fd, basic.jrange.as_ref()?.clone_box(), 0))); + if !dispatched.is_some_and(|rows| !rows.is_empty() && rows.iter().all(|&row| row)) { + return false; + } + let mut targets = Vec::new(); + if model.build_addresses(fd, indirect, &mut targets, None, None).is_err() || targets.len() <= 1 { + return false; + } + let jump = self.opaddress.get_offset(); + if !targets.iter().all(|target| { + let offset = target.get_offset(); + offset != 0 + && offset.abs_diff(jump) <= 0xffff + && fd.get_arch().get_load_image_value(target, 4).is_ok() + }) { + return false; + } + fd.get_override_mut().insert_multistage_jump(self.opaddress.clone()); + fd.get_override_mut().insert_multistage_targets(self.opaddress.clone(), targets); + fd.set_restart_pending(true); + true + } + /// Choose the values that label \b this table after a late re-recovery: /// the flow-time values when the new model reproduces their rows, the new /// model's own values when they rebuild the table, and otherwise none (the /// model is dropped, so the trivial model labels each case by address). /// - /// Stands in for the upstream multistage restart on a table mismatch - /// (`Override::insertMultistageJump`/`setRestartPending`, not ported). - fn choose_label_values(&mut self, fd: &mut Funcdata, rows: &LabelRows) { + /// Stands in for the upstream multistage restart on a table mismatch that + /// [`Self::request_multistage`] does not restart. + fn choose_label_values(&mut self, fd: &mut Funcdata, rows: &LabelRows, warn: bool) { let indop = self.indirect.unwrap(); let Some(model) = self .jmodel @@ -6246,7 +6309,7 @@ impl JumpTable { self.origmodel = None; return; } - self.drop_model_for_rows(fd); + self.drop_model_for_rows(fd, warn); } /// The table addresses whose block is the current default destination. @@ -6260,8 +6323,9 @@ impl JumpTable { /// Drop the model of a table whose flow-time rows no recovered model can /// label, so the trivial model labels each case by address: by the - /// flow-time table values that reach it. - fn drop_model_for_rows(&mut self, fd: &mut Funcdata) { + /// flow-time table values that reach it. `warn` adds the header warning; a + /// pass that has requested a multistage restart is not the final one. + fn drop_model_for_rows(&mut self, fd: &mut Funcdata, warn: bool) { self.case_values = self .block2addr .iter() @@ -6273,9 +6337,11 @@ impl JumpTable { self.case_values.sort(); self.case_values.dedup(); self.jmodel = None; - fd.warning_header( - "Switch model does not reproduce the recovered table; cases are labelled by address", - ); + if warn { + fd.warning_header( + "Switch model does not reproduce the recovered table; cases are labelled by address", + ); + } } /// Recover the case labels for \b this jump-table @@ -6382,12 +6448,8 @@ impl JumpTable { } /// Check if this jump-table requires an additional recovery stage - /// (C++ `JumpTable::checkForMultistage`, `jumptable.cc:3003`). - /// - /// STUB(W4): the override-table query (`fd->getOverride().queryMultistageJumptable`) - /// is W4; the structural guards are faithful and short-circuit before the - /// query whenever possible. - pub fn check_for_multistage(&mut self, _fd: &Funcdata) -> KunaResult { + /// (C++ `JumpTable::checkForMultistage`, `jumptable.cc:2865`). + pub fn check_for_multistage(&mut self, fd: &Funcdata) -> KunaResult { if self.addresstable.len() != 1 { return Ok(false); } @@ -6400,10 +6462,14 @@ impl JumpTable { if self.recover_count > 1 { return Ok(false); } - // fd->getOverride().queryMultistageJumptable(indirect->getAddr()) STUB(W4) - Err(KunaError::lowlevel( - "JumpTable::checkForMultistage: Override::queryMultistageJumptable is W4", - )) + let Some(addr) = self.indirect.and_then(|op| fd.obank().get(op)).map(|op| op.get_addr().clone()) else { + return Ok(false); + }; + if fd.get_override().query_multistage_jumptable(&addr) { + self.partial_table = true; // Mark that we need additional recovery + return Ok(true); + } + Ok(false) } /// Perform a sanity check on recovered address targets diff --git a/decompiler/crates/kuna-decomp/src/substrate/funcdata_block.rs b/decompiler/crates/kuna-decomp/src/substrate/funcdata_block.rs index 8c7721ec1e..bfa239d27b 100644 --- a/decompiler/crates/kuna-decomp/src/substrate/funcdata_block.rs +++ b/decompiler/crates/kuna-decomp/src/substrate/funcdata_block.rs @@ -2604,7 +2604,12 @@ impl Funcdata { let addr2 = partial.obank().get(partop).unwrap().get_addr().clone(); jt.set_indirect_op_addr(partop, addr2); let res = if jt.is_partial() { - jt.recover_multistage(partial) + let targets = self.get_override().multistage_targets(&addr).map(<[Address]>::to_vec); + jt.recover_multistage(partial).map(|()| { + if let Some(targets) = targets { + jt.kuna_adopt_multistage_targets(targets); + } + }) } else { jt.recover_addresses(partial) }; diff --git a/docs/spec/00-overview.md b/docs/spec/00-overview.md index 9cf1ea2124..9ec15b2c6a 100644 --- a/docs/spec/00-overview.md +++ b/docs/spec/00-overview.md @@ -3484,15 +3484,14 @@ study summarized in `docs/history.md`; every row below is re-verified against th | (kuna/angr) P7/P8 structuring fallback | degraded re-run | the region structurer cannot collapse the graph to a single root | nothing; `sblocks` is re-seeded clean | `decompiler/crates/kuna-decomp/src/p8_structure/blockaction.rs (ActionBlockStructure)` falls back to `CollapseStructure` after `decompiler/crates/kuna-decomp/src/p8_structure/region_structurer.rs (run_region_structurer)` declines | | P0 → everything, the outer loop | knowledge-store re-run | an operator/agent writes an assertion (`option`, `kassert`, override) and re-decompiles | the entire P0 store | `decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_assert.rs (Dispatch)`; the console rebuilds the IR per `decompile`, re-seeding stashed facts — `decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs (decompile_func_full_with_override_dyn)` (§0.8 is when the rebuild is skipped) | -**Not implemented in kuna** (theory-only, kept for the record): the upstream -jump-table *size-mismatch* restart — `matchModel` finding the recovered model's -size differs from the flow-recovered address table would persist -`Override::insertMultistageJump` and restart. In kuna the mismatch keeps the -flow-recovered addresses and does not restart -(`decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs (JumpTable::match_model)`, -a documented stub); the Override store already carries the hint surface -(`decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs -(Override::insert_multistage_jump)`) with no live producer. +**Partly implemented in kuna**: the upstream jump-table *size-mismatch* +restart. When `matchModel` finds a flow-time table of one address whose late +model has more entries, kuna persists `Override::insert_multistage_jump` (with +the late model's targets) and restarts, at most once per jump +(`decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs (JumpTable::request_multistage)`; +§2.3 *Multistage recovery* has the guard and reach checks). Every other size +mismatch keeps the flow-recovered addresses and does not restart +(`decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs (JumpTable::match_model)`). Mechanisms are mutually disabling by design: no restart, and no dead-code-delay bump, fires inside the jump-table sub-decompilation — the sub-query must answer diff --git a/docs/spec/02-lift-and-flow.md b/docs/spec/02-lift-and-flow.md index fe882fe3bd..037d40c556 100644 --- a/docs/spec/02-lift-and-flow.md +++ b/docs/spec/02-lift-and-flow.md @@ -1652,11 +1652,35 @@ Without the option the branch is truncated to a `CALLIND` carrying (the `jumpassist` user-op family) and the manual `JumpBasicOverride` model are unported shells: `jumptable.rs (JumpTable::set_override)` and the `` arm of `jumptable.rs (JumpTable::decode)` return errors, and -`recover_model` walks only JumpBasic/JumpBasic2 and, when `option constselectjump` is on, the constant-destination model above (Trivial exists only as the label-time fallback). Likewise upstream's -multistage *restart* accounting — persisting a table whose size disagrees at -`matchModel` time and restarting the whole function — is a recorded loss: kuna -keeps the flow-recovered addresses instead and lets the row check below decide -which values label them (`jumptable.rs (JumpTable::match_model)`). +`recover_model` walks only JumpBasic/JumpBasic2 and, when `option constselectjump` is on, the constant-destination model above (Trivial exists only as the label-time fallback). + +**Multistage recovery.** A flow-time table can come out with a single address +when the partial clone folds its index to a constant. One case is a stack slot +stored before a call, whose address is passed to that call and read back as the +switch index. The partial does not yet know the call's inputs, so it collapses +the INDIRECT the call casts over the slot and reads the stored value. As +upstream does at `matchModel` time, a table of one address whose late model has +more entries records a multistage hint for its jump and restarts the function +(`jumptable.rs (JumpTable::match_model)`). On the next flow, +`flow.rs (FlowInfo::check_multistage_jumptables)` queues the hinted table again +and recovers it through `jumptable.rs (JumpTable::recover_multistage)`. (kuna) The +second stage runs on the same kind of partial, so it can fold the index again. +The hint therefore also carries the targets the late model built +(`overrides.rs (Override::multistage_targets)`). A second stage that again finds +one address adopts them (`jumptable.rs (JumpTable::kuna_adopt_multistage_targets)`), +so flow follows every case and the late check below labels them. Each jump is +restarted at most once. A jump is not restarted unless every value in the +late model's range passes the guards on its path +(`jumptable.rs (JumpBasicModel::values_pass_guards)`) and every target is +mapped and within the `0xffff` reach of the jump. A range wider than its guards, +as `switchguardbound` can recover under a lower-bound guard, would otherwise +turn rows the code never dispatches into cases. The restarting pass still +labels its table, without the model-drop warning, because it is the pass +printed when the drive's restart budget runs out. On that last pass +`decompile_drive.rs (run_pipeline)` keeps the analysis instead of re-following +flow, and adds `Exceeded maximum restarts with more pending` to the header. +This is a correctness repair, without an option: before it, such a switch kept +one case, and its other indexes fell through to the code after the switch. ### The late check: labels, normalization folding, guard folding From f7e74589717e3793108c98e4f9d48b8b9b245a84 Mon Sep 17 00:00:00 2001 From: Paulo Matias Date: Wed, 28 Oct 2026 22:38:09 -0300 Subject: [PATCH 09/11] [AUTOMATED] Fix SPARC branch conditions clobbered by delay slots (#1287) A flag-writing or register-writing SPARC delay slot could decide the branch it sits under. This example should clear a byte only when both arguments are below the limit: ```sh python3 -c "from pathlib import Path; Path('/tmp/sparc-delay.bin').write_bytes(bytes.fromhex('80a2000a0a80000580a2400a81c3e00801000000010000002abffffdc02ac00081c3e00801000000'))" kuna decompile /tmp/sparc-delay.bin 0x1000 --raw-image \ --target sparc:BE:32:default --base 0x1000 --assert-strict \ --assert 'prototype 0x1000 void sub_1000(unsigned int previous, unsigned int current, unsigned int limit, unsigned char *out)' ``` Before the fix the body lost the first comparison (`if (current < limit) *out = 0;`); it now keeps the `limit <= previous` early return. ## Fix - The icc/xcc `cs`, `neg` and `vs` conditions exported the live flag register, so a delay slot that set the flags decided the branch. They are now copied before `delayslot(1)`, like the neighboring condition operands. - The non-annulled register branches (`brz`/`brlez`/`brlz`/`brnz`/`brgz`/`brgez`) tested `rs1` after expanding the delay slot, so `brz,pt %o0,L; mov 5,%o0` decompiled to `return 5;`. The register test is now taken before the slot. gcc -O2 sparc64 emits this shape far more often than the flag case (zlib `deflate`/`deflateInit2_` and sqlite3 `sqlite3MutexInit` regain lost NULL and callback checks). - Annulled forms, FBfcc/FBPfcc and `jmpl` were already correct. No option: SLEIGH correctness repair. ## Tests - kuna-sleigh emulator tests for the flag and register-branch cases (fail on the old spec), and a `tests/stages/` testcase. - SPARC compiler digests and the gh6990-returnpair lift fixture refreshed (unique-space offsets only). - 150 SPARC objects (gcc-12/clang sparc and sparcv9, zlib, sqlite3): 194 functions change, all gcc -O2 delay-slot sites; non-SPARC output is byte-identical; speed within noise. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW --- decompiler/crates/kuna-base/src/xml.rs | 3 +- .../kuna-slacomp/tests/golden/README.md | 6 +- .../kuna-slacomp/tests/golden/compiler.sha256 | 6 +- .../crates/kuna-sleigh/tests/integration.rs | 3 + .../tests/sparc_delay_slot_condition.rs | 187 +++++++++++ docs/baseline-stages.json | 9 +- docs/spec/02-lift-and-flow.md | 11 + .../Sparc/data/languages/SparcV9.sinc | 24 +- tests/golden/vectors/lift/README.md | 6 + .../golden/vectors/lift/gh6990-returnpair.txt | 300 +++++++++--------- tests/stages/kuna-sparc-delay-condition.xml | 57 ++++ 11 files changed, 443 insertions(+), 169 deletions(-) create mode 100644 decompiler/crates/kuna-sleigh/tests/sparc_delay_slot_condition.rs create mode 100644 tests/stages/kuna-sparc-delay-condition.xml diff --git a/decompiler/crates/kuna-base/src/xml.rs b/decompiler/crates/kuna-base/src/xml.rs index afeb893156..cd95891975 100644 --- a/decompiler/crates/kuna-base/src/xml.rs +++ b/decompiler/crates/kuna-base/src/xml.rs @@ -2113,7 +2113,8 @@ mod tests { // and kuna-gowritebarrier / the Go GC write-barrier diamond folds (option gowritebarrier) // and kuna-ppc-lwarx-reservation / PowerPC lwarx sets the reservation its stwcx. checks // and kuna-ppc64-ldarx-encoding / generic PowerPC ldx and ldarx decode at their own opcodes - assert_eq!(count, 591, "corpus file count drifted"); + // and kuna-sparc-delay-condition / a flag-writing delay slot cannot change its branch + assert_eq!(count, 592, "corpus file count drifted"); } /// ~20 representative SLEIGH spec files across varied processors diff --git a/decompiler/crates/kuna-slacomp/tests/golden/README.md b/decompiler/crates/kuna-slacomp/tests/golden/README.md index 83809f03d4..02c734f0dc 100644 --- a/decompiler/crates/kuna-slacomp/tests/golden/README.md +++ b/decompiler/crates/kuna-slacomp/tests/golden/README.md @@ -19,7 +19,11 @@ The SPARC entries include kuna's destination corrections for `fdtoi`, `fqtoi`, `fstox` and `fqtox`. The first three were regenerated with Ghidra's C++ compiler on the corrected sources, which reproduces the previous hashes on the unmodified sources; the `fqtox` correction was regenerated with `slacomp`, -which reproduces those C++ digests. +which reproduces those C++ digests. The current digests also cover the branch +conditions sampled before a delay slot (`icc`/`xcc` `cs`, `neg` and `vs`, and +the non-annulled register branches). They were regenerated with the recorded +C++ compiler, which reproduces the previous SPARC digests on the previous +sources; `slacomp` produces the same digests. The x86 entries include the width-specific x87 integer-conversion operations, the control-word writes of `FXRSTOR` and `XRSTOR`/`XRSTORS`, and the signed diff --git a/decompiler/crates/kuna-slacomp/tests/golden/compiler.sha256 b/decompiler/crates/kuna-slacomp/tests/golden/compiler.sha256 index fa2a8a0174..8fa385ccb0 100644 --- a/decompiler/crates/kuna-slacomp/tests/golden/compiler.sha256 +++ b/decompiler/crates/kuna-slacomp/tests/golden/compiler.sha256 @@ -25,9 +25,9 @@ db22e1cd2be9d4c96c1fd32a93d87e562cca50faabe5807463e1cdeca55b83b9 specs/Ghidra/P ecea205201328f704b229f7ac810f800fb9c8805f5de912fc308dbb4d35334ab specs/Ghidra/Processors/MC6800/data/languages/H6309.slaspec d4e2cfa83ada5e9e19abcecb5fb360adf45c4b5b7b15761c1dec16d72540e82e specs/Ghidra/Processors/NDS32/data/languages/nds32be.slaspec 66abba4eb5f35b3e85687c43c5dd6202cc2bce04959524c49aca52a2ac2668ec specs/Ghidra/Processors/NDS32/data/languages/nds32le.slaspec -# SPARC fdtoi/fqtoi/fstox/fqtox destination corrections. -e33bf3dd3d7cd90f20461915f51981e757af41e177ae9178ce95f05e865999db specs/Ghidra/Processors/Sparc/data/languages/SparcV9_32.slaspec -de8de7baf9348c29f6c2ca3dcfe07266a6651d8b1398ec646c1f9872b2a73107 specs/Ghidra/Processors/Sparc/data/languages/SparcV9_64.slaspec +# SPARC fdtoi/fqtoi/fstox/fqtox destination corrections and delay-slot branch-condition snapshots. +087e1846df849cbe332f44093bed30b1e78a66db75c085b1ff9d3c989108390a specs/Ghidra/Processors/Sparc/data/languages/SparcV9_32.slaspec +a724b2694d40000b078e3824f24c02d7a7b8d558bc1a5064b254bc5a5030a51a specs/Ghidra/Processors/Sparc/data/languages/SparcV9_64.slaspec 21f6f960e061de299c8266872af3ec621f539fed8b748f5137128b19d8e87e28 specs/Ghidra/Processors/SuperH/data/languages/sh-1.slaspec ec462b829a1469bb1c7e4a4d54dea350c9642290de5bf5fa5476bd0233204d40 specs/Ghidra/Processors/SuperH4/data/languages/SuperH4_be.slaspec a1aa90d64bbaa0aac2e8208c4e31e4e295b2d997b1973b6c6100704f87d3c4b0 specs/Ghidra/Processors/SuperH4/data/languages/SuperH4_le.slaspec diff --git a/decompiler/crates/kuna-sleigh/tests/integration.rs b/decompiler/crates/kuna-sleigh/tests/integration.rs index 5dd2ce53a3..9c1359f009 100644 --- a/decompiler/crates/kuna-sleigh/tests/integration.rs +++ b/decompiler/crates/kuna-sleigh/tests/integration.rs @@ -29,6 +29,9 @@ mod ppc_ldx_ldarx_encoding; #[path = "ppc_nested_operands.rs"] mod ppc_nested_operands; +#[path = "sparc_delay_slot_condition.rs"] +mod sparc_delay_slot_condition; + #[path = "sparc_float_to_integer.rs"] mod sparc_float_to_integer; diff --git a/decompiler/crates/kuna-sleigh/tests/sparc_delay_slot_condition.rs b/decompiler/crates/kuna-sleigh/tests/sparc_delay_slot_condition.rs new file mode 100644 index 0000000000..b72ff6471b --- /dev/null +++ b/decompiler/crates/kuna-sleigh/tests/sparc_delay_slot_condition.rs @@ -0,0 +1,187 @@ +//! SPARC branches sample their condition before a delay slot that rewrites it. +use std::{cell::RefCell, path::PathBuf, rc::Rc}; + +use kuna_base::{address::Address, error::KunaResult, space::AddrSpace}; +use kuna_sleigh::{ + emulate::{BreakTableCallBack, Emulate, EmulatePcodeCache}, + globalcontext::ContextInternal, + loadimage::LoadImage, + memstate::{MemoryHashOverlay, MemoryState}, + sleigh::Sleigh, + translate::Translate, +}; + +struct Bytes(Vec); +impl LoadImage for Bytes { + fn get_file_name(&self) -> &str { + "synthetic-sparc-delay-condition" + } + fn get_arch_type(&self) -> Vec { + Vec::new() + } + fn adjust_vma(&mut self, _: i64) {} + fn load_fill(&mut self, out: &mut [u8], addr: &Address) -> KunaResult<()> { + for (i, byte) in out.iter_mut().enumerate() { + *byte = addr + .get_offset() + .checked_sub(0x1000) + .and_then(|offset| self.0.get(offset as usize + i)) + .copied() + .unwrap_or(0); + } + Ok(()) + } +} + +struct Machine { + emulator: EmulatePcodeCache, + state: Rc>, + ram: Rc, +} + +impl Machine { + fn new(bits: u32, branch: u32, slot: u32) -> Self { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.."); + let sla = std::fs::read(root.join(format!( + "specs/Ghidra/Processors/Sparc/data/languages/SparcV9_{bits}.sla" + ))) + .unwrap(); + let bytes: Vec<_> = [branch, slot, 0x01000000, 0x01000000, 0x01000000] + .into_iter() + .flat_map(u32::to_be_bytes) + .collect(); + let mut sleigh = Sleigh::new(Box::new(Bytes(bytes)), Box::new(ContextInternal::new())); + sleigh.initialize_from_sla(&sla).unwrap(); + let manager = sleigh.base().manager_rc(); + let ram = Rc::clone(manager.get_space_by_name("ram").unwrap()); + let trans: Rc = Rc::new(sleigh); + let mut state = MemoryState::new(Rc::clone(&trans)); + for name in ["ram", "register", "unique"] { + let space = Rc::clone(manager.get_space_by_name(name).unwrap()); + state.set_memory_bank(Rc::new(RefCell::new(MemoryHashOverlay::new( + space, 8, 64, 64, None, + )))); + } + let state = Rc::new(RefCell::new(state)); + let breaks = Rc::new(RefCell::new(BreakTableCallBack::new(Rc::clone(&trans)))); + let emulator = EmulatePcodeCache::new(trans, manager, Rc::clone(&state), breaks); + Machine { + emulator, + state, + ram, + } + } + + fn step(&mut self, registers: &[(&str, u64)]) -> u64 { + { + let mut state = self.state.borrow_mut(); + for (name, value) in registers { + state.set_value_by_name(name, *value).unwrap(); + } + } + self.emulator + .set_execute_address(&Address::new(Rc::clone(&self.ram), 0x1000)) + .unwrap(); + self.emulator.execute_instruction().unwrap(); + self.emulator.get_execute_address().get_offset() + } + + fn read(&self, name: &str) -> u64 { + self.state.borrow().get_value_by_name(name).unwrap() + } +} + +fn check_branch(bits: u32, predicted: bool, extended: bool, annul: bool, cond: u32, flag: &str) { + let word = (u32::from(annul) << 29) + | (cond << 25) + | (if predicted { 1 << 22 } else { 2 << 22 }) + | (if extended { 2 << 20 } else { 0 }) + | 4; + let mut machine = Machine::new(bits, word, 0x80a04002); + for incoming in [0, 1] { + for slot_flag in [0, 1] { + let (left, right) = if slot_flag == 0 { + (0, 0) + } else if cond == 7 { + (if extended { 1u64 << 63 } else { 1u64 << 31 }, 1) + } else { + (0, 1) + }; + let mut registers: Vec<(&str, u64)> = ["i_cf", "i_nf", "i_vf", "x_cf", "x_nf", "x_vf"] + .into_iter() + .map(|name| (name, 0)) + .collect(); + registers.extend([(flag, incoming), ("g1", left), ("g2", right)]); + let next = machine.step(®isters); + let context = format!("SPARC{bits} predicted={predicted} extended={extended} annul={annul} {flag}={incoming} slot={slot_flag}"); + assert_eq!( + next, + if incoming == 1 { 0x1010 } else { 0x1008 }, + "{context}" + ); + assert_eq!( + machine.read(flag), + if annul && incoming == 0 { + incoming + } else { + slot_flag + }, + "{context}" + ); + } + } +} + +#[test] +fn integer_branches_use_incoming_flags_and_publish_delay_slot_flags() { + for bits in [32, 64] { + for annul in [false, true] { + for (cond, flag) in [(5, "cf"), (6, "nf"), (7, "vf")] { + check_branch(bits, false, false, annul, cond, &format!("i_{flag}")); + check_branch(bits, true, false, annul, cond, &format!("i_{flag}")); + if bits == 64 { + check_branch(bits, true, true, annul, cond, &format!("x_{flag}")); + } + } + } + } +} + +#[test] +fn register_branches_test_the_register_before_the_delay_slot_rewrites_it() { + let rconds: [(u32, fn(i64) -> bool); 6] = [ + (1, |v| v == 0), + (2, |v| v <= 0), + (3, |v| v < 0), + (5, |v| v != 0), + (6, |v| v > 0), + (7, |v| v >= 0), + ]; + for bits in [32u32, 64] { + let mask = if bits == 32 { + u64::from(u32::MAX) + } else { + u64::MAX + }; + for annul in [false, true] { + for (rcond, holds) in rconds { + for slot in [-1i64, 0, 1] { + let branch = + (u32::from(annul) << 29) | (rcond << 25) | (3 << 22) | (1 << 14) | 4; + let mov_g1 = 0x82102000 | (slot as u32 & 0x1fff); + let mut machine = Machine::new(bits, branch, mov_g1); + for incoming in [-1i64, 0, 1] { + let taken = holds(incoming); + let next = machine.step(&[("g1", incoming as u64 & mask)]); + let context = format!( + "SPARC{bits} rcond={rcond} annul={annul} g1={incoming} slot={slot}" + ); + assert_eq!(next, if taken { 0x1010 } else { 0x1008 }, "{context}"); + let g1 = if annul && !taken { incoming } else { slot }; + assert_eq!(machine.read("g1"), g1 as u64 & mask, "{context}"); + } + } + } + } + } +} diff --git a/docs/baseline-stages.json b/docs/baseline-stages.json index f1c4ee0b65..26c30cfdce 100644 --- a/docs/baseline-stages.json +++ b/docs/baseline-stages.json @@ -1,7 +1,7 @@ { "data_footer": [ - 3228, - 3228 + 3233, + 3233 ], "passing": [ "data:64-bit logical shift retains unsigned destination", @@ -1588,6 +1588,11 @@ "data:SIGNEDREAD #6: the unsigned compare keeps its unsigned operand", "data:SIGNEDREAD-A64 #1: the word index sign-extends sig - 1", "data:SIGNEDREAD-A64 #2: no zero extension of sig - 1", + "data:SPARC delay condition: current-value comparison remains live", + "data:SPARC delay condition: guarded clear remains a byte store", + "data:SPARC delay condition: previous-value comparison remains live", + "data:SPARC delay condition: register branch keeps its nonzero path", + "data:SPARC delay condition: register branch keeps its zero path", "data:SPARC-EXTERNAL-TAIL #1: off still follows the jump into missing extern bytes", "data:SPARC-EXTERNAL-TAIL #2: default recovers the named tail call and delay-slot argument", "data:SPARC-REGISTER-PAIR #1: the loop index in %l4 steps by itself at the default", diff --git a/docs/spec/02-lift-and-flow.md b/docs/spec/02-lift-and-flow.md index 037d40c556..8077320c09 100644 --- a/docs/spec/02-lift-and-flow.md +++ b/docs/spec/02-lift-and-flow.md @@ -37,6 +37,17 @@ the upper register bank. The rules apply in both language variants. See the [SPARC V9 instruction mapping](https://docs.oracle.com/cd/E19963-01/html/821-1607/sparcv9-32853.html). This is a SLEIGH correctness repair, without an inference option. +SPARC integer branch conditions snapshot the ICC/XCC carry, negative, and +overflow flags before expanding the delay slot. A flag-writing slot still +publishes its new flags to subsequent instructions, but cannot change the +current branch's decision. Ordinary and predicted branches share these +condition operands; annulled branches retain their conditional slot execution. +The non-annulled register branches (`brz`, `brlez`, `brlz`, `brnz`, `brgz`, +`brgez`) likewise test `rs1` before the delay slot, so a slot that rewrites +that register keeps its new value without changing the branch. Annulled forms +already tested the register first. This is a SLEIGH correctness repair, +without an inference option. + PowerPC `lwarx` sets the reservation (`RESERVE`, its length and its address) before loading the word, as the ISA constructor does. `stwcx.` and `stdcx.` store only while `RESERVE` is set, so a load-reserve/store-conditional retry diff --git a/specs/Ghidra/Processors/Sparc/data/languages/SparcV9.sinc b/specs/Ghidra/Processors/Sparc/data/languages/SparcV9.sinc index 664b25bf90..80426818a9 100644 --- a/specs/Ghidra/Processors/Sparc/data/languages/SparcV9.sinc +++ b/specs/Ghidra/Processors/Sparc/data/languages/SparcV9.sinc @@ -741,11 +741,11 @@ icc: "l" is cond=0x3 { tmp:1=(i_nf ^^ i_vf); export tmp; } icc: "gu" is cond=0xc { tmp:1=!(i_cf || i_zf); export tmp; } icc: "leu" is cond=0x4 { tmp:1=(i_cf || i_zf); export tmp; } icc: "cc" is cond=0xd { tmp:1=!(i_cf); export tmp; } -icc: "cs" is cond=0x5 { export i_cf; } +icc: "cs" is cond=0x5 { tmp:1=i_cf; export tmp; } icc: "pos" is cond=0xe { tmp:1=!(i_nf); export tmp; } -icc: "neg" is cond=0x6 { export i_nf; } +icc: "neg" is cond=0x6 { tmp:1=i_nf; export tmp; } icc: "vc" is cond=0xf { tmp:1=!(i_vf); export tmp; } -icc: "vs" is cond=0x7 { export i_vf; } +icc: "vs" is cond=0x7 { tmp:1=i_vf; export tmp; } xcc: "a" is cond=0x8 { tmp:1=1; export tmp; } xcc: "ne" is cond=0x9 { tmp:1=!x_zf; export tmp; } @@ -757,11 +757,11 @@ xcc: "l" is cond=0x3 { tmp:1=(x_nf ^^ x_vf); export tmp; } xcc: "gu" is cond=0xc { tmp:1=!(x_cf || x_zf); export tmp; } xcc: "leu" is cond=0x4 { tmp:1=(x_cf || x_zf); export tmp; } xcc: "cc" is cond=0xd { tmp:1=!(x_cf); export tmp; } -xcc: "cs" is cond=0x5 { export x_cf; } +xcc: "cs" is cond=0x5 { tmp:1=x_cf; export tmp; } xcc: "pos" is cond=0xe { tmp:1=!(x_nf); export tmp; } -xcc: "neg" is cond=0x6 { export x_nf; } +xcc: "neg" is cond=0x6 { tmp:1=x_nf; export tmp; } xcc: "vc" is cond=0xf { tmp:1=!(x_vf); export tmp; } -xcc: "vs" is cond=0x7 { export x_vf; } +xcc: "vs" is cond=0x7 { tmp:1=x_vf; export tmp; } cc: icc is cc1=0 & cc0=0 & icc { export icc; } cc: xcc is cc1=1 & cc0=0 & xcc { export xcc; } @@ -771,12 +771,12 @@ d16off: reloc is d16hi & d16lo [reloc = inst_start+4*((d16hi<<14) | d16lo);] { predict: ",pt" is p=1 { } predict: ",pn" is p=0 { } -:brz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x1 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 == 0) goto d16off;} -:brlez^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x2 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 s<= 0) goto d16off;} -:brlz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x3 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 s< 0) goto d16off;} -:brnz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x5 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 != 0) goto d16off;} -:brgz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x6 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 s> 0) goto d16off;} -:brgez^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x7 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 s>= 0) goto d16off;} +:brz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x1 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 == 0); delayslot(1); if (taken) goto d16off;} +:brlez^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x2 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 s<= 0); delayslot(1); if (taken) goto d16off;} +:brlz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x3 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 s< 0); delayslot(1); if (taken) goto d16off;} +:brnz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x5 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 != 0); delayslot(1); if (taken) goto d16off;} +:brgz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x6 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 s> 0); delayslot(1); if (taken) goto d16off;} +:brgez^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x7 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 s>= 0); delayslot(1); if (taken) goto d16off;} :brz^",a"^predict RS1,d16off is op=0 & a=1 & bit28=0 & rcond2=0x1 & op2=0x3 & RS1 & d16off & predict { if (RS1 != 0) goto inst_next; delayslot(1); goto d16off;} :brlez^",a"^predict RS1,d16off is op=0 & a=1 & bit28=0 & rcond2=0x2 & op2=0x3 & RS1 & d16off & predict { if (RS1 s> 0) goto inst_next; delayslot(1); goto d16off;} diff --git a/tests/golden/vectors/lift/README.md b/tests/golden/vectors/lift/README.md index afd26f961f..d5555a811d 100644 --- a/tests/golden/vectors/lift/README.md +++ b/tests/golden/vectors/lift/README.md @@ -26,6 +26,12 @@ operand loads. Thirteen lines change only unique-space offsets by `0xc00`. The refreshed runtime output uses the x86 specs independently matched against the pinned C++ compiler; both focused fixture replays pass. +`gh6990-returnpair.txt` was refreshed for the SPARC delay-slot branch-condition +correction. The SPARC specs it replays match the pinned C++ compiler's digests. +The function contains no `cs`, `neg`, `vs` or register branch, so its +instructions, operations and temporary aliasing are unchanged; 150 lines change +only unique-space offsets by `0xc00`. + ## Regeneration ```bash diff --git a/tests/golden/vectors/lift/gh6990-returnpair.txt b/tests/golden/vectors/lift/gh6990-returnpair.txt index cedb35e524..2f9100681b 100644 --- a/tests/golden/vectors/lift/gh6990-returnpair.txt +++ b/tests/golden/vectors/lift/gh6990-returnpair.txt @@ -21,7 +21,7 @@ space join index=7 size=4 wordsize=1 bigendian=1 space stack index=8 size=4 wordsize=1 bigendian=1 # golden lift ram:0x32148 80 (kuna_goldengen.cc; see tests/golden/vectors/lift/README.md) insn ram:0x32148 4 - INT_ADD (unique,0x28e00,4) (register,0x38,4) (const,0xffffff48,4) + INT_ADD (unique,0x29a00,4) (register,0x38,4) (const,0xffffff48,4) CBRANCH - (const,0x59,4) (register,0x5020,1) INT_MULT (unique,0x0,4) (register,0x6054,4) (const,0x10,4) INT_ADD (unique,0x100,4) (unique,0x0,4) (const,0x0,4) @@ -128,7 +128,7 @@ insn ram:0x32148 4 COPY (register,0x58,4) (const,0x0,4) COPY (register,0x5c,4) (const,0x0,4) INT_ADD (register,0x6054,4) (register,0x6054,4) (const,0x1,4) - COPY (register,0x38,4) (unique,0x28e00,4) + COPY (register,0x38,4) (unique,0x29a00,4) insn ram:0x3214c 4 insn ram:0x32150 4 INT_SUB (register,0x6054,4) (register,0x6054,4) (const,0x1,4) @@ -234,7 +234,7 @@ insn ram:0x32154 8 INT_ADD (unique,0xb100,4) (register,0x3c,4) (const,0x8,4) RETURN - (unique,0xb100,4) insn ram:0x3215c 4 - INT_ADD (unique,0x28e00,4) (register,0x38,4) (const,0xffffffa0,4) + INT_ADD (unique,0x29a00,4) (register,0x38,4) (const,0xffffffa0,4) CBRANCH - (const,0x59,4) (register,0x5020,1) INT_MULT (unique,0x0,4) (register,0x6054,4) (const,0x10,4) INT_ADD (unique,0x100,4) (unique,0x0,4) (const,0x0,4) @@ -341,7 +341,7 @@ insn ram:0x3215c 4 COPY (register,0x58,4) (const,0x0,4) COPY (register,0x5c,4) (const,0x0,4) INT_ADD (register,0x6054,4) (register,0x6054,4) (const,0x1,4) - COPY (register,0x38,4) (unique,0x28e00,4) + COPY (register,0x38,4) (unique,0x29a00,4) insn ram:0x32160 4 insn ram:0x32164 4 INT_SUB (register,0x6054,4) (register,0x6054,4) (const,0x1,4) @@ -444,221 +444,221 @@ insn ram:0x32164 4 LOAD (register,0x5c,4) (const,register,8) (unique,0xa000,4) COPY (register,0x5010,1) (const,0x1,1) insn ram:0x32168 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3216c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32170 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32174 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32178 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3217c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32180 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32184 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32188 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3218c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32190 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32194 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32198 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3219c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321a0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321a4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321a8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321ac 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321b0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321b4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321b8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321bc 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321c0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321c4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321c8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321cc 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321d0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321d4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321d8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321dc 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321e0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321e4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321e8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321ec 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321f0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321f4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321f8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321fc 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32200 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32204 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32208 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3220c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32210 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32214 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32218 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3221c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32220 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32224 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32228 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3222c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32230 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32234 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32238 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3223c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32240 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32244 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32248 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3224c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32250 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32254 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32258 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3225c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32260 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32264 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32268 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3226c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32270 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32274 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32278 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3227c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32280 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32284 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32288 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) diff --git a/tests/stages/kuna-sparc-delay-condition.xml b/tests/stages/kuna-sparc-delay-condition.xml new file mode 100644 index 0000000000..f507280763 --- /dev/null +++ b/tests/stages/kuna-sparc-delay-condition.xml @@ -0,0 +1,57 @@ + + + + +80a2000a0a80000580a2400a81c3e0080100000001000000 +2abffffdc02ac00081c3e00801000000 +02ca00049010200581c3e0080100000081c3e00890022001 + + + + + +(previous < limit|limit <= previous) +(current < limit|limit <= current) +\*out = 0; +return 6; +return 5; + From f183a333c04124a7cbdae09430f83200390636df Mon Sep 17 00:00:00 2001 From: mahaloz Date: Thu, 29 Oct 2026 01:44:54 +0000 Subject: [PATCH 10/11] [AUTOMATED] Keep libgccsigs out of the aggressive preset on purpose `aggressive_carries_every_default_off_option` requires a default-off option to be in the preset or listed with a reason. libgccsigs must not reach the default path: a SPARC32 caller returning arithmetic on the quotient would lose words. List it in EXCLUDED_ON_PURPOSE and in docs/modes.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW --- decompiler/crates/kuna-decomp/src/p0_knowledge/modes.rs | 6 +++++- docs/modes.md | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/decompiler/crates/kuna-decomp/src/p0_knowledge/modes.rs b/decompiler/crates/kuna-decomp/src/p0_knowledge/modes.rs index 7b5c4523cc..5f07ba3e94 100644 --- a/decompiler/crates/kuna-decomp/src/p0_knowledge/modes.rs +++ b/decompiler/crates/kuna-decomp/src/p0_knowledge/modes.rs @@ -595,7 +595,11 @@ mod tests { // the web front-end, the benchmark) materialize every target they walk // anyway, so preset membership would buy nothing there and has not been // measured on them. Flip it into the preset with that measurement. - "lazyfuncs"]; + "lazyfuncs", + // `libgccsigs` costs a SPARC32 caller that returns arithmetic on + // the `__divdi3` quotient its low word or its whole return (see its + // catalog row), so the preset must not turn it on. + "libgccsigs"]; /// Default-off options that predate this test and are **not** in the preset, /// i.e. are currently unreachable on the default path. Each is a genuine open diff --git a/docs/modes.md b/docs/modes.md index 78e6e4ae7a..7761aa96b8 100644 --- a/docs/modes.md +++ b/docs/modes.md @@ -196,9 +196,16 @@ not, and the invariant test's `EXCLUDED_ON_PURPOSE` list is the authority: the preset would make a near-neutral judgment call the default output under 500 KiB. +- **`libgccsigs`** — the SPARC32 `long long __divdi3(long long, long long)` + contract fixes a caller that stores only the quotient's low word, but a caller + that returns arithmetic on the quotient (`a / b + 1`, `-(a / b)`, + `f ? a / b : c`) then keeps only the high word or returns nothing, where the + default returns every word. + These therefore stay manual per-run opt-ins (`--option v850indirectbranch on`, `--option dwarf_lines on`, `--option formatstring full`, `--option ifuncfpret on`, -`--option msvcstrappend on`, `--option bejoin on`, `--option regsigned on`) +`--option msvcstrappend on`, `--option bejoin on`, `--option regsigned on`, +`--option libgccsigs on`) even under `--mode aggressive`; a named `--option` still wins over the preset by last-write precedence. For `formatstring` only the `full` loop above is excluded: its `static` default reads the same format constants out of the image at load, so From 5ee4669e93d7ecf4299dcd0286d4cea678daea53 Mon Sep 17 00:00:00 2001 From: mahaloz Date: Thu, 29 Oct 2026 16:17:00 +0000 Subject: [PATCH 11/11] [AUTOMATED] Do not read a consumed wide call result as a return whole_pair took a declared eight-byte call result as the caller's return whenever both words still reached the RETURN. On PowerPC32, MIPS and ARM big-endian the result stays in the return registers after the caller uses it, so `void store(long long a,long long *p){*p=f(a);}`, `void global(long long a){g64=f(a);}` and `if (f(a) > 100) g();` became `long long` functions returning the stored value, where main prints `void`. Require every reader of the call result, through moves, slices and phis, to be a RETURN or an input a later call's parameter recovery has not made an argument (upstream's onlyOpUse/checkCallDoubleUse rule; the little-endian models already behave this way). Also split contained return guards only when the high-first pair has four-byte words, the only shape whole_pair can complete; a 64-bit BE model keeps upstream's single guard. The libgccsigs row now counts the SPARC clamp shape among the callers that lose words (14 of 21). New test: a_consumed_wide_result_is_not_a_return (ARM BE, -O0/-O2). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW --- .../crates/kuna-cli/tests/libgcc_divdi3.rs | 49 ++++++++++++- decompiler/crates/kuna-decomp/phases.toml | 2 +- .../kuna-decomp/src/p3_dataflow/heritage.rs | 8 +-- .../src/p4_calls/kuna_retcallhalf.rs | 70 ++++++++++++++++++- .../tests/fixtures/phase_catalog.json | 2 +- docs/options.md | 2 +- docs/spec/01-program-prep.md | 3 +- docs/spec/04-calls-and-prototypes.md | 12 ++-- 8 files changed, 134 insertions(+), 14 deletions(-) diff --git a/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs b/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs index 2f7321951b..cb8dd88465 100644 --- a/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs +++ b/decompiler/crates/kuna-cli/tests/libgcc_divdi3.rs @@ -1,5 +1,6 @@ //! Signed DI-mode call results survive stores and full-width caller returns -//! under `libgccsigs`, which is off by default. +//! under `libgccsigs`, which is off by default, and a caller that consumes a +//! declared wide result does not return it. use crate::common; use object::{ write, Architecture, BinaryFormat, Endianness, RelocationFlags, SymbolFlags, SymbolKind, @@ -325,3 +326,49 @@ int main(void) {{ } } } + +#[test] +fn a_consumed_wide_result_is_not_a_return() { + if Command::new("clang").arg("--version").output().is_err() { + return; + } + let root = tempfile::tempdir().unwrap(); + let fixture = root.path().join("consumed.c"); + std::fs::write( + &fixture, + r#" +long long f(long long a); +long long g64; +long long plain(long long a){return f(a);} +void store(long long a,long long *p){*p=f(a);} +void global(long long a){g64=f(a);} +void twice(long long a,long long *p){p[0]=f(a);p[1]=f(a+1);} +"#, + ) + .unwrap(); + for level in ["-O0", "-O2"] { + let image = root.path().join("consumed.o"); + common::process::required_output( + Command::new("clang") + .args(["-target", "armeb-linux-gnueabi", "-fno-pic", level, "-c"]) + .arg(&fixture) + .arg("-o") + .arg(&image), + ); + for (function, signature) in [ + ("plain", "long long plain("), + ("store", "void store("), + ("global", "void global("), + ("twice", "void twice("), + ] { + let (output, error, status) = common::run_kuna(&[ + "decompile", image.to_str().unwrap(), function, "--json", + "--assert", "prototype f long long f(long long a)", + ]); + assert_eq!(status, 0, "{error}"); + let document: serde_json::Value = serde_json::from_str(&output).unwrap(); + let body = document["functions"][0]["code"].as_str().unwrap(); + assert!(body.starts_with(signature), "{level} {function}: {body}"); + } + } +} diff --git a/decompiler/crates/kuna-decomp/phases.toml b/decompiler/crates/kuna-decomp/phases.toml index 94664ee2c4..ad94554eb7 100644 --- a/decompiler/crates/kuna-decomp/phases.toml +++ b/decompiler/crates/kuna-decomp/phases.toml @@ -5982,7 +5982,7 @@ strength = "HARD" rewind = "P1" issue = "pr-1252-libgcc-divdi3" summary = "Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract." -use_when = "Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 13 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly." +use_when = "Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal and nothing else reads the quotient (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 14 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, a clamp (`if (x > 100) x = 100;`), and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly." example = "option libgccsigs on" source_decompiler = "kuna" inspiration = "libgcc/compiler-rt `__divdi3` (di_int __divdi3(di_int a, di_int b), di_int = int64_t); Ghidra ApplyDataArchiveAnalyzer" diff --git a/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs b/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs index 2e02b6fdc1..bce3a3b1c7 100644 --- a/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs +++ b/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs @@ -2759,9 +2759,9 @@ impl Heritage { } /// Guard the biggest model output contained by a wider heritaged range. On a - /// model whose pair returns the high word first, also guard every other - /// contained output: a locked register-pair call result covers both words, - /// and [`crate::kuna_retcallhalf::whole_pair`] may need the second one. + /// model whose four-byte pair returns the high word first, also guard every + /// other contained output: a locked register-pair call result covers both + /// words, and [`crate::kuna_retcallhalf::whole_pair`] may need the second one. fn guard_returns_overlapping( &mut self, fd: &mut crate::funcdata::Funcdata, @@ -2770,7 +2770,7 @@ impl Heritage { ) { let split = fd.get_func_proto().has_model() && crate::kuna_bejoin::wide_pair(fd.get_func_proto().model(), fd.get_arch().manage()) - .is_some(); + .is_some_and(|pair| pair.hi.1 == 4 && pair.lo.1 == 4); self.guard_returns_in_range(fd, addr, size, addr, size, true, split); } diff --git a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs index ba5438815d..94d8517e50 100644 --- a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs +++ b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs @@ -334,13 +334,81 @@ fn whole_words( _ => {} } match (call_word(data, hi, 4), call_word(data, lo, 0)) { - (Some(h), Some(l)) if h == l => Some(true), + (Some(h), Some(l)) if h == l && only_returned(data, h) => Some(true), (Some(_), None) if lo_literal => Some(false), (None, Some(_)) if hi_literal => Some(false), _ => None, } } +/// Every reader of a call result, through moves, slices and phis, is a RETURN. +/// A caller that stores, passes or tests the result consumed it, so the words +/// left in the return registers are no evidence that it returns them. +fn only_returned(data: &Funcdata, result: VarnodeId) -> bool { + let mut seen = std::collections::BTreeSet::new(); + let mut work = vec![result]; + while let Some(cur) = work.pop() { + if !seen.insert(cur) { + continue; + } + if seen.len() > 64 { + return false; + } + let Some(v) = data.vbank().get(cur) else { return false }; + if v.is_persist() { + return false; + } + for reader in v.descend_iter() { + let Some(op) = data.obank().get(reader) else { return false }; + let moves = match op.code() { + OpCode::CPUI_RETURN => continue, + OpCode::CPUI_CALL | OpCode::CPUI_CALLIND => { + if pending_argument(data, reader, op.get_slot(cur)) { + continue; + } + false + } + OpCode::CPUI_COPY + | OpCode::CPUI_SUBPIECE + | OpCode::CPUI_PIECE + | OpCode::CPUI_MULTIEQUAL + | OpCode::CPUI_INT_ZEXT + | OpCode::CPUI_INT_SEXT => true, + OpCode::CPUI_INDIRECT => op.get_in(0) == Some(cur), + OpCode::CPUI_INT_OR if op.get_in(0) == op.get_in(1) => true, + OpCode::CPUI_INT_ADD | OpCode::CPUI_INT_OR | OpCode::CPUI_INT_XOR => (0..2).any(|slot| { + op.get_in(slot) + .is_some_and(|other| other != cur && is_zero(data, other, ZERO_DEPTH)) + }), + _ => false, + }; + match op.get_out() { + Some(out) if moves => work.push(out), + _ => return false, + } + } + } + true +} + +/// An input of a later call whose parameter recovery has not made it an +/// argument, as upstream's `checkCallDoubleUse` treats it. +fn pending_argument(data: &Funcdata, call: OpId, slot: i32) -> bool { + let Some(index) = data.get_call_specs_index(call) else { return false }; + let spec = data.get_call_specs(index); + let placeholder = spec.get_stack_placeholder_slot(); + if slot <= 0 || slot == placeholder || !spec.is_input_active() { + return false; + } + let active = spec.active_input(); + let trial = slot - if placeholder < 0 || slot < placeholder { 1 } else { 2 }; + if trial < 0 || trial >= active.get_num_trials() { + return false; + } + let trial = active.get_trial(trial); + !(trial.is_checked() && trial.is_active()) +} + /// A word at its ABI position in one locked integer call result. fn call_word(data: &Funcdata, word: (VarnodeId, u64), offset: u64) -> Option { let value = data.vbank().get(word.0)?; diff --git a/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json b/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json index fe5df11bb5..320ede63d9 100644 --- a/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json +++ b/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json @@ -216,7 +216,7 @@ {"option": "libcsigs", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-libcsigs", "summary": "Seed the MEASURED extension of the built-in libc prototype table: the 539 further signatures the 28-entry libproto table does not carry (exit, gettext, dcgettext, error, strerror, strdup, strcasecmp, close, open, read, write, stat, getenv, syslog, the FORTIFY __printf_chk/__fprintf_chk/__memcpy_chk family, __errno_location, __stack_chk_fail, the *at family, the SELinux, account, xattr, socket and pthread surfaces, ...). Two mechanical selection rules built it, both over the frozen decbench C corpus and neither one a judgement about which names matter: the first 206 were ranked by PLT call-site frequency (>= 100 call sites, or >= 3 of the 1587 cases where IDA scores a perfect type_match and kuna does not); the 333 that widened it are every name that is an UNDEFINED FUNC symbol in at least 3 of the corpus's 665 dynamically linked binaries and is not already carried. A name in either rule is admitted only if its platform declaration reduces WHOLE to the width-stable Ty vocabulary -- a slot with no honest spelling (off_t, time_t, long long, char) rejects the declaration rather than approximating it, which is why lseek, time, strtoll, mmap, signal and qsort are deliberately absent. Every signature is reduced by script from gcc -aux-info over the platform headers (_GNU_SOURCE + _FORTIFY_SOURCE=2), never written from memory. Applied ONLY to names the image imports and does not itself define, so a program's own function that happens to share a libc spelling is never retyped.", "use_when": "On (default) types the arguments of every common libc call, so a caller whose parameter only flows into one gets a concrete type (char *path) instead of the inferred unsigned long, and the callee's return type is known. Flip OFF when a binary links a private library that reuses libc spellings with different signatures and is NOT statically linked (the pass already skips any name the image defines), or to ablate this table's contribution to a type-recovery difference; off declares exactly the names the 28-entry base table carries and nothing more. It is NOT an ablation of everything a declared prototype does to a call site: the stack placeholder that a call carries until its stack space is heritaged is stripped unconditionally, not by this option, so a base-table name renders with exactly its declared arity in both arms. A declared arity is also an assertion about the CALL SITE, not only about the types: a name this table carries renders with exactly the arguments the signature states, where an unsignatured name renders with whatever argument recovery found. That is the point where a wrong row would show, so the derivation, the rejected set and the whole-corpus diff of the widening live in docs/features/libcwiden/.", "example": "option libcsigs off", "source_decompiler": "kuna", "inspiration": "Ghidra ApplyDataArchiveAnalyzer (.gdt library archives); IDA Pro's til type libraries; measured on decbench C corpus (1587 ida-perfect/kuna-imperfect type_match cases)", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a caller's parameter is unsigned long where it is only ever passed to a libc function that takes a char */int", "a libc call renders its string argument as a bare 0x... constant instead of a literal", "a wrapper around a libc function loses its return value or return type", "__printf_chk / __fprintf_chk arguments are shifted by the fortify flag", "an exit or __stack_chk_fail call shows phantom arguments"]}, {"option": "libctypes", "values": ["off", "opaque", "glibc"], "default": "opaque", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-libctypes", "summary": "Spell the libc/POSIX aggregate pointers in the built-in prototype tables by NAME instead of void *: FILE *, DIR *, stat *, dirent *, passwd *, group *, tm *, option *, timespec *, timeval *, sigaction *, sigset_t *, mbstate_t *, termios *, sockaddr *, pthread_mutex_t *, obstack *, spwd *, utmpx *, utmp *, re_pattern_buffer *, lconv *, statfs *. The Ty vocabulary of those tables is width-stable, so every aggregate pointer in them is a void * today - honest about the width and silent about the pointee, which is the one thing a declaration actually knows. The retarget is enumerated slot by slot (a blanket void *->FILE * would type the va_list of vasprintf/vsnprintf/__vfprintf_chk/verr as a stream), and it brings the stdio names neither shipped table carries: __uflow, fgetc, rewind, freopen, popen, pclose, getdelim, flockfile, funlockfile. A second round of slots was mined from the benchmark corpus's own debug twins - which pointer-to-named-struct variables ground truth holds, and which libc slot each could be reached from - and adds the rest of the stream surface (fseeko, ftello, fread_unlocked, fputc_unlocked, feof_unlocked, fgets_unlocked, setbuf, vfprintf, __getdelim), the record-at-a-time and reentrant halves of the three account databases (getpwent/fgetpwent/putpwent/getpwnam_r and their group and spwd twins), more tm/timespec/termios slots, the regex entry points, localeconv and statfs/fstatfs. Two kinds of slot are left out because naming them splits the caller's frame object: the two-element timespec/timeval arrays of utimensat/futimens/utimes/futimesat, and further sigset_t slots, which a caller hands &sa.sa_mask of a struct sigaction local. Those names are new to both shipped tables, so each supplies an ARITY where there was none as well as a pointee. One table is matched against a name the image DEFINES rather than imports: the five _obstack_* entry points, which are the implementation-reserved half of obstack.h and can only be glibc's or gnulib's copy of the same file. That is what makes obstack reachable at all - gnulib links its copy in and the linker exports the symbols, so a stripped grep, tar or coreutils image carries _obstack_newchunk in .dynsym and nothing else in it says what the first argument addresses; obstack is the widest aggregate in the measured ground truth after FILE. Each named type is interned as a shell carrying its real glibc x86-64 width (FILE 216, stat 144, dirent 280, ...), never width 0 - a zero-width pointee keeps a PTRSUB alive through RulePtrsubUndo and the printer then emits the functional PTRSUB(p,0x28) form - and is left INCOMPLETE so the exported .h declares it opaque rather than as a struct with a width and no members. On an image that carries debug info the platform's own definition is used instead, fields and all: the pass runs after the DWARF importer and adopts an aggregate of the declared width already held under the name (or under glibc's _IO_FILE spelling of FILE) - complete or still being populated - and declines the signature rather than touch a name held by anything else, a different width included. The third value, `glibc`, fills in the nine aggregates whose layout the platform PUBLISHES and an application is meant to read - FILE, stat, timespec, timeval, tm, passwd, group, option, dirent - with their real x86-64 members, so an access renders under its own name (`f->_IO_read_ptr`, `st->st_size`, `opt[i].name`) instead of `f->field_0x8`, and the loaded value takes the FIELD's type rather than whatever the cast said - `st_size` is a signed long, so a wrapper that returns -1 on failure stops printing its sentinel as 0xffffffffffffffff. Every offset and width is measured with offsetof/sizeof against the installed headers, not restated from memory (docs/features/libctypes/glibc.md). Two claims the tables refuse to make: no field is spelled as a pointer to the struct being filled (_IO_FILE::_chain is void * - completing a struct re-keys it into a new Rc, so a self-pointer taken mid-completion would strand the program on two stream types), and no layout is installed unless it is TRUE of the image - x86-64 ELF whose .dynstr names glibc (the libc.so.6 soname or a GLIBC_2.x symbol version). musl, another libc and a statically linked image all keep the opaque shells, which for a static image costs nothing because these tables are matched against imported names only. The WIDTHS are a claim too, and about the caller's frame, not only about offsets inside the shell: a named pointer handed a frame object's address grows that object to the declared width, which is right on x86-64 against glibc (`char v1 [12]` becomes `termios v1` where the program touched twelve bytes of the struct `tcgetattr` fills) and on i386 folded the caller's neighbouring locals into an 8-byte timespec declared as 16 (`v1._12_4_ = a0 + 2`). So nothing at all is named - no slot, no stream symbol, no shell - unless the image is an x86-64 ELF not dynamically linked against another C library (no .dynstr, or one naming glibc); i386, ARM, AArch64, MIPS, PE and Mach-O get the shipped void * tables, byte for byte what `off` gives, and a name declared there by hand does too. A named vote is also held to what the caller does through the pointer. A libc aggregate is often the FIRST member of a larger object (grep's kwset begins with its obstack, an rsyslog instance record with its mutex), whose address is then the one the call reads; where the caller reads that argument past the aggregate's end or before its start, forms an address at or past the end, or steps it by a constant at least as large as the aggregate that is not exactly its size (coreutils wc walks &fstatus[i].st and reads `failed` 8 bytes before it), that call's vote falls back to void * (`kuna_libcfit`) and the declared type decides only the call's cast; type propagation likewise refuses to carry the named pointer onto a value read past the end, so `kwsincr` stops printing every kwset member as an element of an obstack array (`&v1[0x1b].field_0x38`). DIR, sigaction, sigset_t, mbstate_t, termios, sockaddr and pthread_mutex_t stay opaque under `glibc` too: glibc publishes no layout for DIR, and the rest are reserved words no caller reads by name. The same tables also type the stdio STREAMS themselves, which no prototype slot can reach: `stdin`/`stdout`/`stderr` are storage, not an argument, so a function that never calls a typed stdio function sees only an `undefined8` word, and on a shared object the slot has no name at all. Under any enabled value the pass emits a typed DATA symbol for each stream slot, read off the DYNAMIC RELOCATION that binds it - which is also the evidence that the name is the C library's and not a global the program spells `stdout` itself. A COPY relocation names a .bss word the run-time loader fills with libc's own `FILE *stdout`, so that slot is `FILE *` under the stream's own name; a GLOB_DAT on an UNDEFINED symbol names a GOT word holding the stream's ADDRESS, so it is `FILE **` under the minted name `stdout_ptr` - calling that one `stdout` would make the emitted `*stdout_ptr` read as an indirection the program does not perform. Because `stdout_ptr` is kuna's coinage and not the image's, the GLOB_DAT arm declines when either symbol table (.symtab as much as .dynsym) already spells it, rather than printing two addresses under one identifier. Linked ELF only, under the same width gate as the rest of the table, and a spelling that occurs twice in .dynsym is declined outright. The slots point at the same `FILE` the rest of the table uses, so `opaque` gives `stdout->field_0x28` and `glibc` `stdout->_IO_write_ptr`, and an image whose own debug info holds a different `FILE` contributes no stream symbol rather than a contradictory one.", "use_when": "On by default (opaque): a pointer the callee's own libc declaration names carries that name - get_prefix(FILE *f) instead of sub_2f30(long a0) on a stripped -O2 coreutils reader, fopen/fdopen/popen results as FILE *, stat/lstat/fstat filling a stat *, getopt_long reading an option * table, readdir returning a dirent *, localtime a tm *, an obstack handle as obstack * even where gnulib linked the obstack code into the program. Flip OFF to ablate the table: to check whether a pointee name came from this declaration rather than from inference, when a target statically links a private function under a libc spelling that the imports-only restriction did not catch (it would then be typed against libc's aggregate, not its own), or when a decompile-project export has to stay as close to compilable as it gets - the shells are deliberately INCOMPLETE, so the exported .c reads fields out of a type its own .h declares opaque (measured +58 cc errors on an ls export - 51 of them from reading fields out of the incomplete shells, the rest from a type name that collides with a function name of its own in the exported body; the .h itself is 0 errors either way). Off is byte-identical to the shipped void * tables, because with the gate off the pass does not run and not one named shell is interned. No datatest loads a file, so the 675 assertions cannot see this row either way; the stage corpus can, and tests/stages/kuna-libctypes.xml pins it in both arms. The flip's evidence is the corpus sweep in docs/features/libctypes/record.json. Flip to `glibc` for the field NAMES: on an x86-64 ELF against glibc it installs the published layouts of FILE/stat/timespec/timeval/tm/passwd/group/option/dirent, turning `*(unsigned char **)&f->field_0x8` into `f->_IO_read_ptr` and `*(unsigned long *)&st->field_0x30` into `st->st_size`, and giving the loaded value the field's type. Prefer it when reading stdio-heavy or stat-heavy code; stay on `opaque` when a decompile-project export must not declare a type the toolchain's own headers also declare. On a target that is not x86-64 against glibc every value is inert: no slot is named at all, because the widths are not that ABI's. The default also NAMES and TYPES the stream slots: `*(long *)(stdout + 0x28)` becomes `*(long *)&stdout->field_0x28` (`stdout->_IO_write_ptr` under `glibc`), and a shared object's `*(long *)(*dat_3fe0 + 0x28)` becomes `*(long *)&(*stdout_ptr)->field_0x28`. That reach is the function that makes no typed stdio call and the shared object whose stream never had a name - over twelve whole binaries 45 lines move, 38 of them a GOT slot gaining its name, and eight of the twelve are byte-identical because inference had already typed those streams from a typed stdio call in the same function. Its cost is local merging: a slot that takes `FILE *` stops merging with the unrelated values a scalar local had absorbed, which adds a declaration and renumbers the locals after it. Naming a data object stays `datasyms`' call - with that option off the stream slot goes back to `dat_`, since the type rides on the symbol - so ablate the stream half with `datasyms off` and the whole table with `libctypes off`.", "example": "option libctypes off", "source_decompiler": "ghidra", "inspiration": "Ghidra ApplyDataArchiveAnalyzer applying generic_clib_64.gdt, whose FILE */struct stat * slots carry the header's own pointee; signatures reduced from gcc -aux-info over the platform headers (_GNU_SOURCE + _FORTIFY_SOURCE=2), widths from sizeof/_Alignof", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["a function that only ever passes its argument to a libc stdio call still declares it long or unsigned long", "a fopen or fdopen result is a void * that nothing can name", "a stat or lstat call fills an untyped void * buffer", "getopt_long takes a bare (void *)0x... long-option table", "readdir returns a void * whose fields are casts and offsets", "a stream parameter is void * where the ground truth is FILE *", "a FILE * or stat * renders its members as field_0x8 / field_0x30 instead of _IO_read_ptr / st_size", "a value loaded out of a named libc struct keeps the cast's type instead of the field's", "stdin/stdout/stderr render as an untyped word and their fields as raw offsets ((long)stdout + 0x28)", "a shared object's stream slot has no name at all (*(long *)(*dat_3fe0 + 0x28))", "two functions in one binary disagree about what stdout is, one calling it FILE * and the other a long"]}, {"option": "win32sigs", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "repipe-resource-loader-c-retains", "summary": "Seed built-in Win32 API prototypes onto a PE's imported API names, keyed by ENTRY ADDRESS. kuna carried libc signatures (libproto, libcsigs) and not one Windows API signature, so on a PE every LoadLibraryExW/CreateFileW/WriteFile call reached ActionDefaultParams with an empty prototype and its arguments had to be recovered from the call site alone; where that recovery loses -- the ordinary case for an image that writes its outgoing slots well before the call -- the call renders `LoadLibraryExW()` and the argument stores survive as mapped stack locals, along with the return-address push the call itself makes. The table is ~130 WINAPI exports ranked by an import histogram over the 54 PE images of the RE arena corpus (admitted at >= 5 images), plus the resource/loader family (FindResourceW, LoadResource, SizeofResource, EnumResourceTypesW/NamesW) below that bar. Each signature is reduced to the width-stable Ty vocabulary and a declaration with a slot that has no honest spelling (a by-value LARGE_INTEGER, a DWORD64) is rejected rather than approximated. Address is the key because a PE import is TWO FunctionSymbols -- the size-0 IAT slot the engine constant-folds through and the `FF 25` thunk veneer a direct `call` targets -- and the global by-name query used by libproto answers with the slot, which is not the address ActionDefaultParams asks about. Imported names only, so a program's own function sharing a Win32 spelling is never retyped; no CRT (__cdecl) spelling is admitted, because on x86 the PE default model is __stdcall and a locked arity is also the callee's stack pop.", "use_when": "On by default (DIV-141). On, a Windows PE renders `LoadLibraryExW(v1,0,8)` / `WriteFile(h,buf,n,&written,0)` instead of `LoadLibraryExW()` with the pushed values left behind as `v41 = 0x401baf;`-style stack stores. Flip OFF to ablate the table's contribution -- to check whether a Win32 call's arguments came from the parked signature rather than from recovery, or if a target statically links its own function under a Win32 spelling that the imports-only restriction did not catch. Every non-PE target is byte-identical either way.", "example": "option win32sigs off", "source_decompiler": "kuna", "inspiration": "Ghidra ApplyDataArchiveAnalyzer (.gdt library archives); IDA Pro's til type libraries; import histogram over the 54 PE images of the RE arena corpus", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a Windows API call renders with an empty argument list", "LoadLibraryExW() or CreateFileW() has no arguments but the pushes are visible above it", "a PE function declares dozens of stack locals that only ever hold outgoing call arguments", "CALL fall-through addresses are assigned to stack variables", "a Win32 call's wide string argument renders as a bare 0x... constant", "calleearity and varargstackargs both on and a Windows API call is still argumentless"]}, - {"option": "libgccsigs", "values": ["on", "off"], "default": "off", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "pr-1252-libgcc-divdi3", "summary": "Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract.", "use_when": "Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 13 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly.", "example": "option libgccsigs on", "source_decompiler": "kuna", "inspiration": "libgcc/compiler-rt `__divdi3` (di_int __divdi3(di_int a, di_int b), di_int = int64_t); Ghidra ApplyDataArchiveAnalyzer", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns", "__divdi3 is called with four 32-bit arguments and only %o0 is read back", "*p = v1; after a __divdi3 call where v1 is never written"]}, + {"option": "libgccsigs", "values": ["on", "off"], "default": "off", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "pr-1252-libgcc-divdi3", "summary": "Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract.", "use_when": "Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal and nothing else reads the quotient (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 14 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, a clamp (`if (x > 100) x = 100;`), and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly.", "example": "option libgccsigs on", "source_decompiler": "kuna", "inspiration": "libgcc/compiler-rt `__divdi3` (di_int __divdi3(di_int a, di_int b), di_int = int64_t); Ghidra ApplyDataArchiveAnalyzer", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns", "__divdi3 is called with four 32-bit arguments and only %o0 is read back", "*p = v1; after a __divdi3 call where v1 is never written"]}, {"option": "declaredlibcproto", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-declaredlibcproto", "summary": "Answer a DECLARED function name out of the built-in libc signature tables, so naming a callee also gives it a prototype. `libproto` and `libcsigs` match a name the IMAGE carries -- its FUNC symbols, its imports -- which is exactly the evidence a stripped, statically linked target does not have. There the name exists only because the operator supplied it (`--define-function 0x8048968=ptrace`, `--assert 'function 0x8048968 ptrace'`), and until now that named the entry and nothing more: the call sites still rendered `ptrace()` with no arguments, because the callee's arity has to come from somewhere and a glibc varargs wrapper (`long ptrace(enum __ptrace_request, ...)`, the rest fetched with `va_arg`) cannot yield it to any amount of body analysis. With this on, a declared name is looked up in both tables and the matching signature is parked on the DECLARED ENTRY ADDRESS -- the key `ActionDefaultParams` reads back per call site, and the only key that survives two symbols sharing a name. The imports-only restriction the `libcsigs` table carries is deliberately lifted here: it exists so a coincidental spelling cannot retype a function the image defines itself, and that is a judgement about evidence, which is different once a human or an agent has identified the entry outright.", "use_when": "On by default: naming a callee is the one thing an RE agent always does, and a name that buys no prototype buys almost nothing. The shape that needs it is a stripped or packed image where argument recovery has nothing to work from -- the tell is a call rendered `f()` with the pushed argument slots visible as raw stores on the lines above it (`*(int *)(esp - 4) = ...` four times, then `v += ptrace();`). Structurally inert unless a run declares a function name (`--define-function START=NAME`, `--assert function`, console `function bounds ... as NAME`), so it is byte-identical over both parity corpora, and inert again unless that name is one of the ~230 the built-in tables carry. Flip OFF when a target defines its own function that merely shares a libc spelling and the declaration is about that function, not the library one -- or to check whether the parked signature, rather than recovery, is what moved a call's arguments. An explicit `--assert prototype` on the same function still wins: it is applied after the declaration.", "example": "kuna decompile ./crackme sub_80483b0 --define-function 0x8048968=ptrace", "source_decompiler": "kuna", "inspiration": "Ghidra ApplyDataArchiveAnalyzer (.gdt archives applied on rename); IDA Pro's til type libraries, which retype a function the moment it is given a library name", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a call to a function I named with --define-function still renders with no arguments", "the argument stores are emitted as raw stack writes on the lines above an argumentless call", "naming a callee changes the spelling of the call and nothing else", "calleearity and varargstackargs both on and a stripped statically linked libc call is still argumentless", "a declared ptrace/read/write/open call loses every argument"]}, {"option": "strings", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "code-data-partition", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-strings", "summary": "Run the string-literal analysis pass: detect NUL-terminated ASCII strings (minimum length 4) and plant a typelocked char[N] data symbol at each. The four-character threshold keeps command and mode literals such as quit, show, bind, fork, and dup2 visible while the separate UTF-16 arm retains Ghidra's length-5 default.", "use_when": "On (default) lays char[N] data at detected strings; off leaves those addresses undefined.", "example": "option strings off", "source_decompiler": "kuna", "inspiration": "Ghidra StringsAnalyzer; pwn.college CSCI 4250 module 5 game servers, where four-character commands and libc operation names rendered as addresses", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["string constants render as raw addresses or unnamed data instead of quoted char[N] literals", "no data symbols at ascii runs in rodata", "four-character commands such as quit, show, bind, or fork render as hexadecimal addresses"]}, {"option": "widestrings", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "code-data-partition", "strength": "HARD", "rewind": "P1", "issue": "re-wide-api-string-args", "summary": "Scan the loaded image at 2-byte (UTF-16LE) width as well as 1-byte, and plant a typelocked wchar2[N] data symbol at each wide literal -- the allCharWidths arm of Ghidra's StringsAnalyzer that kuna's 1-byte port left as a documented seam. Read at 1-byte width a UTF-16LE literal is a ONE-CHARACTER string (`L\"ntdll.dll\"` is 6e 00 74 00 ..., whose first NUL closes the run after `n`), so nothing is marked up there, the constant keeps whatever char * the type lattice gave it, and the printer reads to the same NUL: every wide Windows-API argument rendered as its own first character -- LoadLibraryW(\"n\"), FindWindowW(\"O\",0). The widened matcher is the 1-byte MinLengthCharSequenceMatcher over 2-byte little-endian code units and nothing else: the same printable-ASCII recognizer over each unit's low byte, the same require-NUL-end rule, the same minimum length of 5, over exactly the address set the 1-byte pass scans, on even addresses only. The two widths cannot both claim a run -- a wide unit demands a zero high byte, so five consecutive 1-byte-charset bytes never occur inside a wide run -- and the 1-byte facts are committed first regardless, so the established width owns any address both could reach. With the symbol planted the existing printer path does the rest: the char type's size 2 is what emits the L prefix and reads the bytes two at a time.", "use_when": "On by default. A wide (UTF-16) string argument renders as a one-character literal -- LoadLibraryW(\"n\") where the binary says L\"ntdll.dll\", FindWindowW(\"O\",0) where it says L\"OllyDbg - [CPU]\" -- which is most of what a Windows anti-debugging or anti-VM check contains. `kuna strings --encoding utf16` already reported those literals in full while the decompiled C did not, and the difference between the two surfaces is the tell. Flip OFF to restore markup that is exactly the 1-byte pass's -- e.g. on an image whose 16-bit data tables read as plausible wide text, or to ablate this width's contribution.", "example": "option widestrings off", "source_decompiler": "ghidra-upstream", "inspiration": "Ghidra StringsAnalyzer's allCharWidths setting (the UTF-16 arm of MinLengthCharSequenceMatcher); measured on crackmes.one 65acadf3eef082e477ff5ede (ObfuscationFiesta.exe, PE32+), where sub_140002530's LoadLibraryW / FindWindowW anti-debug checks rendered as one-character strings", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["a wide Windows API argument renders as a one-character string literal", "LoadLibraryW(\"n\") or FindWindowW(\"O\",0) in the emitted C", "kuna strings --encoding utf16 reports a literal the decompiled C truncates to its first character", "a UTF-16 string address carries no data symbol while the ascii ones do", "an anti-debugging check names no DLL or window class"]}, diff --git a/docs/options.md b/docs/options.md index 72d5893d23..b3ceefba42 100644 --- a/docs/options.md +++ b/docs/options.md @@ -2418,7 +2418,7 @@ Program-prep enablement: what is discovered, decoded, and named before any funct - **Symptoms:** a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns; __divdi3 is called with four 32-bit arguments and only %o0 is read back; *p = v1; after a __divdi3 call where v1 is never written. - **What it does:** Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract. -- **When to flip:** Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 13 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly. +- **When to flip:** Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal and nothing else reads the quotient (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 14 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, a clamp (`if (x > 100) x = 100;`), and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly. - **Where / provenance:** P1/external-refinement · kuna · correctness-fix · pr-1252-libgcc-divdi3 - **Example:** `option libgccsigs on` diff --git a/docs/spec/01-program-prep.md b/docs/spec/01-program-prep.md index 0ee40ff368..9889d88629 100644 --- a/docs/spec/01-program-prep.md +++ b/docs/spec/01-program-prep.md @@ -1863,7 +1863,8 @@ moves. at load and committed only when the option is on. It ships off because the locked eight-byte result still costs a caller that returns arithmetic on the quotient: §4's whole-pair proof keeps both return words only when each is a - bare slice of the call result or a literal, so `return a / b + 1;`, + bare slice of the call result or a literal and nothing else reads the + quotient, so `return a / b + 1;`, `return -(a / b);` and `return f ? a / b : c;` keep only the high word or return nothing, where the off output returns every word. - **(kuna) Measured libc signatures** (`libcsigs`, diff --git a/docs/spec/04-calls-and-prototypes.md b/docs/spec/04-calls-and-prototypes.md index da88e477cf..b70e843445 100644 --- a/docs/spec/04-calls-and-prototypes.md +++ b/docs/spec/04-calls-and-prototypes.md @@ -2078,8 +2078,8 @@ if no join can be built); more pieces chain PIECEs over contiguous trials. The (kuna) `returnpair` gate intercepts this join — §4.4. A wider heritaged register range can contain several separate output entries. -On a model whose register pair returns the high word first (`kuna_bejoin`'s -`wide_pair`: SPARC, PowerPC, MIPS and ARM big-endian), +On a model whose four-byte register pair returns the high word first +(`kuna_bejoin`'s `wide_pair`: 32-bit SPARC, PowerPC, MIPS and ARM big-endian), `Heritage::guard_returns_overlapping` keeps the extra entries as inactive candidates instead of dropping their bytes or treating the range alone as evidence that the caller returns every word. Every other model keeps @@ -2104,8 +2104,12 @@ other word on every incoming edge. Other paths may replace either word with a literal, including constant-only expressions not yet folded by the simplification pool and the upper zero word of a zero-extension. A lower word inside an extension keeps its source provenance. Unrelated live values do not -establish a pair, and at least one path must return a whole wide value. The -ABI supplies the join order. Explicit +establish a pair, and at least one path must return a whole wide value. +Every reader of that call result, through moves, slices and phis, must be a +RETURN or an input that a later call's parameter recovery has not made an +argument: a caller that stores, passes or tests the result consumed it, so a +void `*p = f(x);` or `G = f(x);` that leaves the words in the return +registers is not a return. The ABI supplies the join order. Explicit caller outputs and competing output classes are unchanged. Provenance rejects single-word truncations that move a different word into the return storage, including the SPARC register-window controls in the regression.