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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion decompiler/crates/kuna-cli/src/loadtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::process::Command;
use kuna_decomp::{
kuna_calltargettype, kuna_dwarfstructs, kuna_dwarfvariants, kuna_dynrelocs, kuna_i386_pie_plt,
kuna_ifuncfpret, kuna_libctypes, kuna_msvcfpconst, kuna_pdatachained, kuna_peordinal,
kuna_goabi, kuna_relocrebase, kuna_rexthunk, kuna_sourcelang, kuna_symbolnamebound, kuna_symbolnamechars,
kuna_goabi, kuna_goreg, kuna_relocrebase, kuna_rexthunk, kuna_sourcelang, kuna_symbolnamebound, kuna_symbolnamechars,
kuna_variantnames,
kuna_symbolnamerepair, kuna_typedepth, options::RELOC_OBJECTS_ENV,
};
Expand Down Expand Up @@ -39,6 +39,7 @@ fn binding(name: &str) -> Option<(&'static str, Encoding)> {
"pdatachained" => (kuna_pdatachained::PDATACHAINED_ENV, DefaultOn),
"rexthunk" => (kuna_rexthunk::REXTHUNK_ENV, DefaultOn),
"goabi" => (kuna_goabi::GOABI_ENV, DefaultOn),
"goreg" => (kuna_goreg::GOREG_ENV, DefaultOn),
"peordinal" => (kuna_peordinal::PEORDINAL_ENV, DefaultOn),
"msvcfpconst" => (kuna_msvcfpconst::MSVCFPCONST_ENV, DefaultOn),
"symbolnamerepair" => (kuna_symbolnamerepair::SYMBOLNAMEREPAIR_ENV, DefaultOn),
Expand Down
4 changes: 4 additions & 0 deletions decompiler/crates/kuna-console/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@ path = "tests/verify_typedepth.rs"
[[test]]
name = "verify_goabi"
path = "tests/verify_goabi.rs"

[[test]]
name = "verify_goreg"
path = "tests/verify_goreg.rs"
1 change: 1 addition & 0 deletions decompiler/crates/kuna-console/src/kuna_console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ pub fn kuna_live_value(conf: &Architecture, option: &str) -> Option<Cow<'static,
"pdatachained" => on_off(conf.analysis_pdatachained),
"rexthunk" => on_off(conf.analysis_rexthunk),
// (kuna `goabi`) Two-valued but spelled auto|off, so it reports its token.
"goreg" => on_off(conf.go_reg),
"goabi" => {
if conf.analysis_goabi {
"auto"
Expand Down
158 changes: 158 additions & 0 deletions decompiler/crates/kuna-console/tests/verify_goreg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//! End-to-end gate for the goroutine-register naming (`goreg`,
//! `docs/multilang.md` W4 — the Go pack's dedicated-register item).
//!
//! The proof, on a REAL stripped Go build under the golang cspec (`goabi`
//! auto): a runtime function that manipulates goroutine state renders its
//! accesses through the named, typed `g` global (`g->stackguard0`), and with
//! `KUNA_GOREG=off` the same function falls back to the phantom-register
//! reading (no `g->` anywhere). `runtime.unlock2` is the witness: it is
//! present in every modern Go binary, named by the pclntab on a stripped
//! build, and its preemption-request store (`g.stackguard0 = stackPreempt`)
//! is the exact idiom the runtime source spells.
//!
//! Own `[[test]]` target: the off arm mutates process env (`EnvGuard`), the
//! `verify_goabi` pattern.

use std::path::PathBuf;
use std::process::{Command, Stdio};

use kuna_console::engine::{bootstrap_from_object, ConsoleProgram};
use kuna_console::ifacedecomp::{
execute, register_decomp_commands, IfaceDecompData, DECOMPILE_MODULE,
};
use kuna_console::ifaceterm::ConsoleCommands;

fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..").canonicalize().unwrap()
}

/// `true` if a runnable `go` toolchain is on PATH.
fn go_available() -> bool {
Command::new("go")
.arg("version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}

/// Any Go program brings the runtime along; the witness is a runtime function.
const MAIN_GO: &str = r#"package main

import (
"fmt"
"os"
)

func main() {
fmt.Println(len(os.Args))
}
"#;

/// Build [`MAIN_GO`] stripped (`-ldflags=-s -w`) for linux/amd64, hermetically.
fn build_go(dir: &std::path::Path, out_bin: &std::path::Path) -> bool {
if std::fs::create_dir_all(dir).is_err() {
return false;
}
if std::fs::write(dir.join("main.go"), MAIN_GO).is_err() {
return false;
}
Command::new("go")
.args(["build", "-ldflags=-s -w", "-o"])
.arg(out_bin)
.arg("main.go")
.current_dir(dir)
.env("GOOS", "linux")
.env("GOARCH", "amd64")
.env("GOCACHE", dir.join(".gocache"))
.env("GOPATH", dir.join(".gopath"))
.env("CGO_ENABLED", "0")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}

/// Bootstrap, commit, decompile `runtime.unlock2`, return the transcript.
fn decompile_unlock2(bin: &str) -> String {
let specs = repo_root().join("specs");
let spec_roots = vec![specs.to_str().unwrap().to_string()];
let mut prog: ConsoleProgram = bootstrap_from_object(bin, "", &spec_roots)
.expect("bootstrap go fixture with built processor specs");
prog.commit_pending_analysis().expect("analysis commit succeeds");
let cmds: Vec<String> = ["load function runtime.unlock2", "decompile", "print C"]
.iter()
.map(|s| s.to_string())
.collect();
let count = cmds.len();
let mut status = ConsoleCommands::into_status(cmds);
register_decomp_commands(&mut status);
{
let data = status.get_data_mut(DECOMPILE_MODULE).unwrap();
let dcp = data.as_any_mut().downcast_mut::<IfaceDecompData>().unwrap();
dcp.conf = Some(prog);
}
for _ in 0..count {
execute(&mut status);
}
status.optr.clone()
}

/// RAII env override (the off arm), restoring the inherited value on drop.
struct EnvGuard(&'static str, Option<std::ffi::OsString>);
impl EnvGuard {
fn set(name: &'static str, value: &str) -> EnvGuard {
let prev = std::env::var_os(name);
std::env::set_var(name, value);
EnvGuard(name, prev)
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match &self.1 {
Some(v) => std::env::set_var(self.0, v),
None => std::env::remove_var(self.0),
}
}
}

#[test]
fn goroutine_state_renders_through_the_named_g_global() {
if !go_available() {
eprintln!("verify_goreg: skipping (`go` not on PATH)");
return;
}
let dir = std::env::temp_dir().join(format!("kuna_goreg_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let out_bin = dir.join("go_stripped");
if !build_go(&dir, &out_bin) {
let _ = std::fs::remove_dir_all(&dir);
panic!("installed Go compiler failed to build the linux/amd64 fixture");
}
let bin = out_bin.to_str().expect("utf8 path");

// Default: goroutine state reads through the named, typed global. The
// stackguard0 store is at the liblink-stated offset 0x10, stable across
// Go releases, so the assertion holds wherever the runtime drifts later.
let on = decompile_unlock2(bin);
assert!(
on.contains("g->"),
"goreg on must render g-relative accesses through the named global; transcript:\n{on}"
);
assert!(
on.contains("g->stackguard0"),
"the preemption-request store renders at its liblink offset; transcript:\n{on}"
);

// Off: the raw reading returns — no named global anywhere in the body.
let _guard = EnvGuard::set("KUNA_GOREG", "off");
let off = decompile_unlock2(bin);
assert!(
!off.contains("g->"),
"goreg off must restore the phantom-register reading; transcript:\n{off}"
);

let _ = std::fs::remove_dir_all(&dir);
}
19 changes: 19 additions & 0 deletions decompiler/crates/kuna-decomp/phases.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7413,6 +7413,25 @@ change_kind = "structure-recovery"
tier = "transform"
symptoms = "every function of a Go binary opens with if (condition) { runtime_morestack_noctxt(); } inside a loop to the entry|go function parameters recovered as phi-fed locals or missing because the entry is a loop head|do-while wrapping an entire go function body with a morestack call in its latch"

[[settable]]
option = "goreg"
values = "on|off"
default = "on"
destructive = false
phase = "P1"
subphase = "environment-binding"
strength = "HARD"
rewind = "P1"
issue = "kuna-multilang-goreg"
summary = "Name and type the goroutine register when the golang compiler spec is active. Under the Go register ABI the current goroutine lives in a pinned register (R14 on amd64, x28 on arm64 - the values the vendored golang register.info files state), and the runtime reads through it constantly; the cspec lists it as unaffected and no pentry names it, so every access rendered as a phantom local (v3; // r14) fed by unexplained arithmetic. With the option on (default) kuna adds the register's storage to the global scope (the cspec global-register mechanism, applied kuna-side so the vendored spec stays pristine) and maps a type- and name-locked `g` symbol typed `runtime.g *` over it, with runtime.g / runtime.gobuf synthesized as shells carrying exactly the fields whose offsets the go1.22 runtime source states for the register-ABI era (through goid at 0x98; sched is the seven-word gobuf). A stackguard load then renders g->stackguard0, and g->m, g->_defer read like the runtime source; an access past the modeled prefix renders as an offset off g, still anchored. Load-time (the KUNA_GOREG env bridge, the goabi seam): the install runs at architecture build, right after the cspec global decode. Structurally inert off-Go by construction - the gate requires the golang compiler spec, which only goabi composes for a Go-detected image, and a golang target with no pinned register (x86-32 reaches g through TLS) is untouched."
use_when = "Leave on: Go runtime internals and every function the gomorestack fold declines render goroutine state as named g-> fields instead of phantom register locals. Flip off to audit the raw register arithmetic, or if a custom runtime's g layout disagrees with the modeled go1.22 prefix."
example = "option goreg off"
source_decompiler = "kuna"
inspiration = "Ghidra's golang register.info current_goroutine entries (never consumed by the decompiler); the cspec <global><register/> mechanism; docs/multilang.md W4"
change_kind = "analysis-enablement"
tier = "analysis"
symptoms = "go functions declare a local commented r14 or x28 that is read but never assigned|stackguard reads render as *(long *)(v3 + 0x10) instead of g->stackguard0|go runtime internals full of unexplained register arithmetic"

[[settable]]
option = "goabi"
values = "auto|off"
Expand Down
43 changes: 43 additions & 0 deletions decompiler/crates/kuna-decomp/src/infra/architecture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2058,6 +2058,9 @@ pub struct Architecture {
/// (kuna `goabi`) Select the vendored golang compiler spec for a Go image
/// (the catalog-visible mirror of the `KUNA_GOABI` load-time env gate).
pub analysis_goabi: bool,
/// (kuna) `option goreg`: name+type the goroutine register as the `g`
/// global when the golang cspec is active. Load-time (env-bridged).
pub go_reg: bool,
/// (kuna) Gate PE import-by-ordinal naming from built-in export tables
/// (`peordinal`); default **on**. Read through the [`crate::kuna_peordinal`]
/// **env var** (import names are resolved inside `load file`, upstream of
Expand Down Expand Up @@ -3193,6 +3196,7 @@ impl Architecture {
analysis_pdatachained: false,
analysis_rexthunk: false,
analysis_goabi: false, // (kuna) option goabi; reset_defaults sets the shipped default (auto)
go_reg: true, // (kuna) option goreg; reset_defaults re-asserts the shipped default (on)
analysis_peordinal: false,
analysis_symbolnamerepair: false,
analysis_symbolnamechars: crate::kuna_symbolnamechars::NameChars::Off,
Expand Down Expand Up @@ -3632,6 +3636,7 @@ impl Architecture {
self.analysis_peordinal = true; // (kuna) an OLEAUT32/WS2_32/WSOCK32/MSVBVM60 import-by-ordinal is named from the built-in export table
self.analysis_rexthunk = true; // (kuna) DIV-179: the `FF 25` one byte into a REX-prefixed tail jump through an import slot is not an import thunk
self.analysis_goabi = true; // (kuna) option goabi default auto: a Go image takes the vendored golang cspec
self.go_reg = true; // (kuna) option goreg default on: the golang cspec's goroutine register renders as `g`
self.analysis_symbolnamerepair = true; // (kuna) DIV: degenerate-symbol-name repair default-ON (it only fires where the load would otherwise fail outright)
self.analysis_symbolnamechars = crate::kuna_symbolnamechars::NameChars::Safe; // (kuna) DIV-94: symbol-name sanitizing defaults to `safe` -- the structural set only, a measured no-op on every name a real toolchain emits
self.analysis_symbolnamebound = Some(crate::kuna_symbolnamebound::DEFAULT_SCOPE_DEPTH); // (kuna) DIV-95 GH-338: symbol-name scope bound default 256 (3.2x the deepest :: nesting found in any real binary measured, 79; unbounded, one name turns 600 KB of .strtab into 292 MB)
Expand Down Expand Up @@ -4920,6 +4925,39 @@ impl Architecture {
/// pointer inference (a separate stub). General over any processor's cspec:
/// the space names are read from the XML and resolved through the engine, with
/// NO processor-name special-casing.
/// (kuna `goreg`) Name and type the golang cspec's goroutine register.
///
/// Mirrors `decode_global`'s `<register>` arm — the register's storage
/// joins the global scope — then maps a type+name-locked `g` symbol typed
/// `runtime.g *` over it (`crate::kuna_goreg`). Gated on the golang cspec
/// being the active compiler spec (the `archid` compiler field) and the
/// `KUNA_GOREG` load-time bridge; a no-register golang target (x86-32
/// reaches `g` through TLS) and every other image are untouched.
fn kuna_install_goreg(&mut self) -> KunaResult<()> {
let Some(reg) = crate::kuna_goreg::goroutine_register(&self.archid) else {
return Ok(());
};
if !crate::kuna_goreg::goreg_enabled() {
return Ok(());
}
let scope = match self.symboltab.get_global_scope() {
Some(s) => s,
None => return Ok(()),
};
let point = self.get_register_varnode(reg.as_bytes())?;
let Some(spc) = point.space.clone() else { return Ok(()) };
let first = point.offset;
let last = first.wrapping_sub(1).wrapping_add(u64::from(point.size));
self.infer_ptr_spaces.push(Rc::clone(&spc));
self.symboltab.add_range(scope, Rc::clone(&spc), first, last);
let gptr = crate::kuna_goreg::g_pointer_type(self.types())?;
let addr = Address::new(spc, first);
let (sid, _eref) =
self.symboltab.add_symbol_mapped(scope, "g", gptr, &addr, &Address::new_invalid())?;
self.symboltab.set_attribute(sid, crate::kuna_goreg::g_symbol_flags());
Ok(())
}

fn decode_global(&mut self) -> KunaResult<()> {
use kuna_base::address::{Range, RangeProperties};
use kuna_base::marshal::{IdRegistry, XmlDecode};
Expand Down Expand Up @@ -7177,6 +7215,11 @@ impl Architecture {
// `mapped|addrtied|persist`. Must run before `adjust_caches` (which only
// resizes per-scope maptables, not the rangetree) — ordering matches C++.
self.decode_global()?;
// (kuna `goreg`) With the golang cspec active, make the goroutine
// register a named, typed global (`g: runtime.g *`) — the same
// global-scope mechanism as a cspec `<global><register/>`, applied
// kuna-side so the vendored spec stays pristine.
self.kuna_install_goreg()?;
self.build_default_proto();
// Share `defaultfp` + the engine address-space manager into the type
// factory so the C-declaration grammar's nested function-pointer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,12 @@ kuna_options! { self, p1;
self.go_morestack = mode.as_u8();
Ok(msg)
},
"goreg" => {
let val = on_or_off(p1)?;
crate::kuna_goreg::set_goreg_env(val);
self.go_reg = val;
Ok(format!("Goroutine register naming turned {}", if val { "on" } else { "off" }))
},
"goabi" => {
let on = crate::kuna_goabi::parse_goabi(p1).map_err(KunaError::parse)?;
crate::kuna_goabi::set_goabi_env(on);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ fn surface_count_is_120() {
}

#[test]
fn settable_count_is_335() {
assert_eq!(kuna_num_settables(), 335);
assert_eq!(SETTABLE_TABLE.len(), 335);
fn settable_count_is_336() {
assert_eq!(kuna_num_settables(), 336);
assert_eq!(SETTABLE_TABLE.len(), 336);
}

#[test]
fn tier_counts_are_120_core_129_transform_86_analysis() {
fn tier_counts_are_120_core_129_transform_87_analysis() {
let mut core = 0;
let mut transform = 0;
let mut analysis = 0;
Expand All @@ -56,7 +56,7 @@ fn tier_counts_are_120_core_129_transform_86_analysis() {
other => panic!("invalid tier {other:?} on {}", s.option),
}
}
assert_eq!((core, transform, analysis), (120, 129, 86));
assert_eq!((core, transform, analysis), (120, 129, 87));
}

#[test]
Expand Down Expand Up @@ -290,6 +290,9 @@ fn option_values_live_value_present_for_154() {
// console-side via `kuna_live_value` (auto|off token), so it has no
// codegen live reader.
"goabi",
// (kuna) The goroutine-register naming — a load-time env bridge
// (`KUNA_GOREG`, the goabi seam), read console-side via kuna_live_value.
"goreg",
// (kuna) The Go pclntab no-return pass gate - an analysis-pass gate
// with no codegen live reader (read console-side via kuna_live_value),
// same as `gopclntab`. Default-ON (Go-only pass).
Expand Down Expand Up @@ -1094,7 +1097,8 @@ fn emit_catalog_json_static_form_brackets_and_commas() {
// +1 for `variantnames`; its P5 row sits beside `dwarfvariants`.
// +1 for `goabi`; its P1 row sits beside `sourcelang`.
// +1 for `gowritebarrier`; its P2 row sits beside `gomorestack`.
assert_eq!(json.matches("},\n").count(), 334);
// +1 for `goreg`; its P1 row sits beside `goabi`.
assert_eq!(json.matches("},\n").count(), 335);
}

#[test]
Expand Down
Loading
Loading