From ada7745097f69676ab6889c39bd4b31dad34efed Mon Sep 17 00:00:00 2001 From: mahaloz Date: Mon, 26 Oct 2026 01:38:09 +0000 Subject: [PATCH] [AUTOMATED] Name the goroutine register as the typed g global (option goreg, default on) Under the Go register ABI the current goroutine lives in a pinned register (R14 on amd64, x28 on arm64 - the vendored golang register.info values); the cspec lists it unaffected and no pentry names it, so every access rendered as a phantom local (v3; // r14) fed by unexplained arithmetic. With the golang cspec active (goabi), the register's storage now joins the global scope (the cspec mechanism applied kuna-side; the vendored spec stays pristine) and a type+name-locked `g` symbol typed runtime.g * maps over it. runtime.g / runtime.gobuf are synthesized shells carrying exactly the fields the go1.22 runtime source states for the register-ABI era (liblink prefix stable across releases; tail go1.22-pinned; past-prefix accesses stay anchored as offsets off g). Load-time env bridge KUNA_GOREG (the goabi seam); structurally inert off-Go; a golang target with no pinned register (x86-32, TLS) is untouched. Measured on the stripped go1.22 hello-world (1,543 fns): phantom // r14 functions 267 -> 23, named g-> field readers 7 -> 297, runtime.unlock2 renders g->stackguard0 = 0xfffffffffffffade exactly as the runtime source spells the preemption request. Speed: interleaved min-of-3 decompile-all on the witness 18.79 s on vs 18.59 s off (+1.1%, a load-time symbol install; budget 5%). Gates: make test PARITY OK, make test-stages PARITY OK (baseline re-record: the renamed catalog #15), make rust-test exit 0 (clean), make check-spec OK, make test-cli 306/306, make lint clean, kuna catalog --check OK. E2E verify_goreg (own [[test]]: the off arm mutates env) builds a real stripped go binary and pins both directions; unit tests pin the shell offsets against the runtime source and the golang-id gate. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WoTv7T2Ep5hmbZcyYSMk1g --- decompiler/crates/kuna-cli/src/loadtime.rs | 3 +- decompiler/crates/kuna-console/Cargo.toml | 4 + .../crates/kuna-console/src/kuna_console.rs | 1 + .../crates/kuna-console/tests/verify_goreg.rs | 158 ++++++++++++ decompiler/crates/kuna-decomp/phases.toml | 19 ++ .../kuna-decomp/src/infra/architecture.rs | 43 ++++ .../src/p0_knowledge/kuna_option_dispatch.rs | 6 + .../src/p0_knowledge/kuna_phases/tests.rs | 16 +- .../src/p1_partition/kuna_goreg.rs | 229 ++++++++++++++++++ .../kuna-decomp/src/p1_partition/mod.rs | 1 + .../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 | 25 ++ tests/stages/kuna-catalog.xml | 2 +- 16 files changed, 516 insertions(+), 13 deletions(-) create mode 100644 decompiler/crates/kuna-console/tests/verify_goreg.rs create mode 100644 decompiler/crates/kuna-decomp/src/p1_partition/kuna_goreg.rs diff --git a/decompiler/crates/kuna-cli/src/loadtime.rs b/decompiler/crates/kuna-cli/src/loadtime.rs index c054ad1a60..f358020072 100644 --- a/decompiler/crates/kuna-cli/src/loadtime.rs +++ b/decompiler/crates/kuna-cli/src/loadtime.rs @@ -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, }; @@ -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), diff --git a/decompiler/crates/kuna-console/Cargo.toml b/decompiler/crates/kuna-console/Cargo.toml index 3ee5a84fe7..e1c589994a 100644 --- a/decompiler/crates/kuna-console/Cargo.toml +++ b/decompiler/crates/kuna-console/Cargo.toml @@ -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" diff --git a/decompiler/crates/kuna-console/src/kuna_console.rs b/decompiler/crates/kuna-console/src/kuna_console.rs index d6a490cc13..566e127069 100644 --- a/decompiler/crates/kuna-console/src/kuna_console.rs +++ b/decompiler/crates/kuna-console/src/kuna_console.rs @@ -289,6 +289,7 @@ pub fn kuna_live_value(conf: &Architecture, option: &str) -> Option 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" diff --git a/decompiler/crates/kuna-console/tests/verify_goreg.rs b/decompiler/crates/kuna-console/tests/verify_goreg.rs new file mode 100644 index 0000000000..4027a45aae --- /dev/null +++ b/decompiler/crates/kuna-console/tests/verify_goreg.rs @@ -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 = ["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::().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); +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); +} diff --git a/decompiler/crates/kuna-decomp/phases.toml b/decompiler/crates/kuna-decomp/phases.toml index fbff4f740a..e715417a5b 100644 --- a/decompiler/crates/kuna-decomp/phases.toml +++ b/decompiler/crates/kuna-decomp/phases.toml @@ -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 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" diff --git a/decompiler/crates/kuna-decomp/src/infra/architecture.rs b/decompiler/crates/kuna-decomp/src/infra/architecture.rs index 5038abc8de..c6babd4d7d 100644 --- a/decompiler/crates/kuna-decomp/src/infra/architecture.rs +++ b/decompiler/crates/kuna-decomp/src/infra/architecture.rs @@ -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 @@ -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, @@ -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) @@ -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 `` 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}; @@ -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 ``, 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 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 72348401e6..ce967ddb58 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 @@ -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); 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 e5469bb161..dff4b15351 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_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; @@ -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] @@ -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). @@ -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] diff --git a/decompiler/crates/kuna-decomp/src/p1_partition/kuna_goreg.rs b/decompiler/crates/kuna-decomp/src/p1_partition/kuna_goreg.rs new file mode 100644 index 0000000000..db5c0c3a62 --- /dev/null +++ b/decompiler/crates/kuna-decomp/src/p1_partition/kuna_goreg.rs @@ -0,0 +1,229 @@ +//! (kuna) The `goreg` option: name the goroutine register (`docs/multilang.md` +//! W4, the Go pack's dedicated-register item). +//! +//! ## The gap it closes +//! +//! Under the register ABI every Go function carries the current goroutine in a +//! pinned register — `R14` on amd64, `x28` on arm64, exactly as the vendored +//! golang `register.info` files state (``) +//! — and the runtime reads through it constantly (`stackguard0` checks the +//! `gomorestack` fold leaves in runtime internals, `g.m`, deferred-panic +//! chains). The cspec lists the register as `unaffected` and no `` +//! names it, so kuna sees a value that is never a parameter and never written: +//! every access rendered as a phantom local (`v3; // r14`) read through +//! unexplained arithmetic. +//! +//! With the option on (the default) and the golang cspec active, the register +//! becomes a NAMED, TYPED global: its storage joins the global scope (the +//! cspec `` mechanism, applied kuna-side so the vendored +//! spec stays pristine) and a `g` symbol typed `runtime.g *` is mapped over +//! it, type- and name-locked. A `stackguard0` load then renders +//! `g->stackguard0`, and `g->m`, `g->_defer`, … read like the runtime source. +//! +//! ## The `runtime.g` shell +//! +//! The synthesized-shell pattern (`docs/multilang.md` W3, the `libctypes` +//! precedent): `runtime.g` and `runtime.gobuf` are interned with exactly the +//! fields whose offsets the go1.22 runtime source states for the register-ABI +//! era (`src/runtime/runtime2.go`, 64-bit layout, through `goid` at 0x98; +//! `gobuf` is seven words). Every field is word-sized except the two `u32`s +//! (`atomicstatus`, `stackLock`), so the offsets are the running sum the +//! builder computes; a unit test pins them against the source-stated values. +//! Fields past `goid` vary more across Go releases and are deliberately not +//! modeled — an access past 0xa0 renders as an offset off `g`, which is still +//! anchored and honest. +//! +//! ## Why an env bridge +//! +//! The install runs at architecture build (right after the cspec `` +//! decode), upstream of every per-function `option` command — the +//! `goabi`/`sourcelang` seam, and the same [`GOREG_ENV`] pattern. The +//! `Architecture::go_reg` field exists for catalog visibility (`kuna_live_value`), +//! and flipping the option in a live session bridges the env for a later +//! `load file`. Structurally inert off-Go: the gate requires the golang cspec +//! (`archid` compiler field `golang`), which only `goabi auto` on a +//! Go-detected image composes. + +use kuna_base::error::KunaResult; +use kuna_base::types::{int4, uint4}; + +use crate::dtype::{type_metatype, TypeFactory, TypeField}; +use crate::options::env_toggle; + +/// Environment variable gating the goroutine-register naming. Absent or any +/// non-off token means **enabled**; `off`/`0`/`false` disables. +pub const GOREG_ENV: &str = "KUNA_GOREG"; + +/// Whether the goroutine-register naming is enabled for this process. +pub fn goreg_enabled() -> bool { + env_toggle(GOREG_ENV, true) +} + +/// Bridge a `set_kuna_option("goreg", val)` toggle to [`GOREG_ENV`] so a later +/// `load file` in the same process sees the choice. +pub fn set_goreg_env(on: bool) { + std::env::set_var(GOREG_ENV, if on { "on" } else { "off" }); +} + +/// The goroutine register for a golang-cspec architecture, by SLEIGH language +/// id — the values the vendored `*.register.info` files state +/// (``). `None` for every non-golang id and for +/// golang ids with no pinned register (x86-32 Go reaches `g` through TLS). +pub fn goroutine_register(archid: &str) -> Option<&'static str> { + // The compiler field is the id's last `:` token; only the golang cspec + // carries these models, and only goabi composes it. + if !archid.ends_with(":golang") { + return None; + } + if archid.starts_with("x86:LE:64:") { + Some("R14") + } else if archid.starts_with("AARCH64:LE:64:") { + Some("x28") + } else { + None + } +} + +/// One verified field of a shell: `(name, size, is_pointer)`. Offsets are the +/// running sum (everything is naturally aligned at these sizes and order). +type ShellField = (&'static str, int4, bool); + +/// `runtime.gobuf` — seven words (go1.22 `runtime2.go`). +const GOBUF_FIELDS: &[ShellField] = &[ + ("sp", 8, false), + ("pc", 8, false), + ("g", 8, false), + ("ctxt", 8, true), + ("ret", 8, false), + ("lr", 8, false), + ("bp", 8, false), +]; + +/// `runtime.g` through `goid` (go1.22 `runtime2.go`, 64-bit): the prefix whose +/// offsets are stated in the source ("offset known to liblink"). `sched` is +/// handled apart (it takes the `runtime.gobuf` shell type). +const G_FIELDS_HEAD: &[ShellField] = &[ + ("stack_lo", 8, false), // 0x00 stack.lo + ("stack_hi", 8, false), // 0x08 stack.hi + ("stackguard0", 8, false), // 0x10 + ("stackguard1", 8, false), // 0x18 + ("_panic", 8, true), // 0x20 + ("_defer", 8, true), // 0x28 + ("m", 8, true), // 0x30 +]; +const G_FIELDS_TAIL: &[ShellField] = &[ + ("syscallsp", 8, false), // 0x70 + ("syscallpc", 8, false), // 0x78 + ("stktopsp", 8, false), // 0x80 + ("param", 8, true), // 0x88 + ("atomicstatus", 4, false), // 0x90 + ("stackLock", 4, false), // 0x94 + ("goid", 8, false), // 0x98 +]; + +/// The byte offset `sched` sits at, and the sizes the builder must reproduce. +pub const G_SCHED_OFFSET: int4 = 0x38; +/// `runtime.gobuf`'s size (seven words). +pub const GOBUF_SIZE: int4 = 0x38; +/// The modeled `runtime.g` prefix size (through `goid`, padded to its align). +pub const G_SIZE: int4 = 0xa0; + +fn push_fields( + types: &dyn TypeFactory, + fields: &mut Vec, + spec: &[ShellField], + mut offset: int4, +) -> KunaResult { + for (name, size, is_ptr) in spec { + let ct = if *is_ptr { + let void = types.get_base(1, type_metatype::TYPE_VOID)?; + types.get_type_pointer(8, void, 1)? + } else { + types.get_base(*size, type_metatype::TYPE_UINT)? + }; + fields.push(TypeField { + ident: fields.len() as int4, + offset, + name: (*name).to_string(), + field_type: ct, + }); + offset += size; + } + Ok(offset) +} + +/// Intern the `runtime.gobuf` + `runtime.g` shells and return `runtime.g *`. +/// +/// Idempotent through the factory's name-keyed interning: a second install of +/// the same layouts resolves to the already-interned types. +pub fn g_pointer_type(types: &dyn TypeFactory) -> KunaResult> { + let gobuf_shell = types.get_type_struct("runtime.gobuf")?; + let mut gb = Vec::new(); + let end = push_fields(types, &mut gb, GOBUF_FIELDS, 0)?; + debug_assert_eq!(end, GOBUF_SIZE); + let gobuf = types.set_fields_struct_raw(&gobuf_shell, gb, Vec::new(), GOBUF_SIZE, 8, 0)?; + + let g_shell = types.get_type_struct("runtime.g")?; + let mut gf = Vec::new(); + let mid = push_fields(types, &mut gf, G_FIELDS_HEAD, 0)?; + debug_assert_eq!(mid, G_SCHED_OFFSET); + gf.push(TypeField { + ident: gf.len() as int4, + offset: G_SCHED_OFFSET, + name: "sched".to_string(), + field_type: gobuf, + }); + let end = push_fields(types, &mut gf, G_FIELDS_TAIL, G_SCHED_OFFSET + GOBUF_SIZE)?; + debug_assert_eq!(end, 0x98 + 8); + let g = types.set_fields_struct_raw(&g_shell, gf, Vec::new(), G_SIZE, 8, 0)?; + types.get_type_pointer(8, g, 1) +} + +/// The flags the mapped `g` symbol locks: the name is the point, and the type +/// is what turns an offset read into a named field. +pub fn g_symbol_flags() -> uint4 { + crate::varnode::varnode_flags::typelock | crate::varnode::varnode_flags::namelock +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn goroutine_register_is_golang_gated() { + assert_eq!(goroutine_register("x86:LE:64:default:golang"), Some("R14")); + assert_eq!(goroutine_register("AARCH64:LE:64:v8A:golang"), Some("x28")); + // The C cspec, a non-Go image, never names one. + assert_eq!(goroutine_register("x86:LE:64:default:gcc"), None); + // x86-32 Go reaches `g` through TLS: golang cspec, no pinned register. + assert_eq!(goroutine_register("x86:LE:32:default:golang"), None); + assert_eq!(goroutine_register(""), None); + } + + #[test] + fn shell_offsets_match_the_runtime_source() { + // The go1.22 runtime2.go 64-bit layout this module's doc derives: + // running sums must land the liblink-stated offsets. + let mut off = 0; + let mut at = std::collections::HashMap::new(); + for (name, size, _) in G_FIELDS_HEAD { + at.insert(*name, off); + off += size; + } + assert_eq!(off, G_SCHED_OFFSET); + off += GOBUF_SIZE; + for (name, size, _) in G_FIELDS_TAIL { + at.insert(*name, off); + off += size; + } + assert_eq!(at["stackguard0"], 0x10, "offset known to liblink"); + assert_eq!(at["stackguard1"], 0x18, "offset known to liblink"); + assert_eq!(at["_panic"], 0x20, "offset known to liblink"); + assert_eq!(at["m"], 0x30, "offset known to arm liblink"); + assert_eq!(at["syscallsp"], 0x70); + assert_eq!(at["atomicstatus"], 0x90); + assert_eq!(at["goid"], 0x98); + assert!(off <= G_SIZE, "modeled prefix fits the declared size"); + assert_eq!(GOBUF_FIELDS.len(), 7, "gobuf is seven words"); + } +} diff --git a/decompiler/crates/kuna-decomp/src/p1_partition/mod.rs b/decompiler/crates/kuna-decomp/src/p1_partition/mod.rs index 60f32c5b2a..690764669c 100644 --- a/decompiler/crates/kuna-decomp/src/p1_partition/mod.rs +++ b/decompiler/crates/kuna-decomp/src/p1_partition/mod.rs @@ -6,6 +6,7 @@ pub mod kuna_cppsig; pub mod kuna_elfmaindecl; // (kuna) an executable's own main declared from its unlocked analysis pub mod kuna_formatstring; +pub mod kuna_goreg; // (kuna) name+type the goroutine register as the `g` global (docs/multilang.md W4) pub mod kuna_litpoolconst; pub mod sleigh_arch; pub mod raw_arch; diff --git a/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs b/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs index b2b06ca472..ea14211bac 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_335_settables() { +fn fixture_has_all_336_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(), 335); + assert_eq!(FIXTURE.matches("\"option\": ").count(), 336); // Every row carries the tier field appended after change_kind. - assert_eq!(FIXTURE.matches("\"tier\": ").count(), 335); + assert_eq!(FIXTURE.matches("\"tier\": ").count(), 336); // ... and the symptoms array appended after tier (C3). - assert_eq!(FIXTURE.matches("\"symptoms\": ").count(), 335); + assert_eq!(FIXTURE.matches("\"symptoms\": ").count(), 336); } #[test] diff --git a/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json b/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json index 562170f080..2a70762cc9 100644 --- a/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json +++ b/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json @@ -295,6 +295,7 @@ {"option": "gonoreturn", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "code-data-partition", "strength": "HARD", "rewind": "P1", "issue": "kuna-multilang-gonoreturn", "summary": "Apply the vendored Go no-return list to the pclntab-recovered function list. The known-no-return pass (noreturn_known) matches GolangFunctionsThatDoNotReturn against the object's symbol tables, but a production Go binary is stripped (-ldflags=-s -w): both tables are gone, the pass emits zero facts, and every entry of the vendored list - runtime.gopanic, runtime.panicshift, runtime.panicunsafestringlen, all of them - goes unused on exactly the binaries it was vendored for. The fallout is structural: a call to runtime.panicshift keeps its fall-through edge, the panic path renders as if the helper returned, and any function whose stack-growth arm physically follows such a call keeps its morestack loop (the gomorestack fold refuses the broken shape, correctly). The names are still in the image - the pclntab names every function - so this pass walks the same table the gopclntab pass recovers names from (same executable-range admission) and matches each (addr, name) against the same vendored list with the same reader, emitting ordinary address-keyed no-return facts. The commit installs pclntab names before the no-return arm resolves, so the facts land whether or not a .symtab exists; on an unstripped binary they are idempotent duplicates of noreturn_known's. Registered ONLY for a Go-detected binary (beside gopclntab), so it is a structural no-op on every non-Go target; `option noreturn_known` continues to gate only the symbol-table pass, keeping the two sources separately flippable.", "use_when": "On (default) truncates flow at runtime panic/exit helpers on stripped Go binaries, exactly as unstripped ones already behave; off restores the fall-through reading of every runtime.panic*/throw/goexit call site (auditing, or a hand-patched runtime whose panic helpers really return).", "example": "option gonoreturn off", "source_decompiler": "kuna", "inspiration": "Ghidra noReturnFunctionConstraints.xml compiler id=golang arm (the list), applied through the pclntab name source its symbol-table reader cannot reach on a stripped binary", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["stripped go binary renders code after a runtime.panicshift or runtime.gopanic call as if the helper returned", "a go panic arm falls through into the next block and the output interleaves panic and normal paths", "a go function keeps its morestack entry loop because the grow arm follows a panic call kuna treats as returning"]}, {"option": "fatstr", "values": ["off", "auto", "always"], "default": "auto", "destructive_as_default": false, "phase": "P5", "subphase": "const-pointer", "strength": "HARD", "rewind": "P5", "issue": "kuna-multilang-fatstr", "summary": "Render a length-carried (ptr, len) constant call-argument pair as a bounded string literal. Rust (&str, &[u8]) and Go (string) carry a string's length beside its pointer and never NUL-terminate the bytes, so the classic literal path - which reads the image until a terminator - refuses them and every string argument renders as a raw rodata address beside a magic length: takes_str((char *)&DAT_00102000, 15). The length is right there in the adjacent argument, so the rule reads exactly that many bytes and routes them through the existing bounded literal mechanism (Funcdata::get_internal_string -> the BUILTIN_STRINGDATA user-op, the constseq path, which needs no terminator): takes_str(\"hello multilang\", 15). A pair is rewritten only when both arguments are p-code constants adjacent in the call's inputs, 2 <= len <= 4096, the pointed-at bytes are READ-ONLY in the image (query_global_properties, the msvcstrappend literal test), and the bytes decode as UTF-8 text with no control characters beyond newline/tab/CR and at least one alphanumeric - or the pointer's facing type carries a text language facet (the multilang W3 side table on the type factory), a type provider's statement that outranks the byte heuristic. The length argument is kept: the call really does pass it. At auto (default) the rule acts only when the image's source language is Rust or Go (the sourcelang fact), a structural no-op elsewhere; always drops the language test (stage-test bytechunks); off keeps the raw pointer. The option also gates the ANALYSIS-side recognizer (pass id fatstrrecords, registered for a rustc-detected image): rustc packs unterminated literals back to back in .rodata, so the NUL-run string scan glues adjacent literals into one wrong row; the static (ptr, len) records that carry every Rust literal's length (a PIE's R_*_RELATIVE slots in .data.rel.ro, absolute pairs on a non-PIE) are re-read as length-BOUNDED string facts, committed ahead of the NUL stream so the glued rows retire and the data symbol at each literal spans exactly the literal. off drops both recognizers; the record facts are dropped at the read-symbols boundary like every analysis gate.", "use_when": "Leave auto: on a Rust or Go binary every constant &str/string argument renders as its literal text instead of a raw &DAT_ address beside a magic length. Use always on a carved bytechunk or raw image with no language signal. Flip off when auditing exact rodata addresses, or if a read-only buffer of coincidentally printable bytes is being misread as text at a call site.", "example": "option fatstr always", "source_decompiler": "kuna", "inspiration": "Rust &str / Go string fat-pointer call ABI measured against the NUL-terminated literal path (PrintC::pushPtrCharConstant requires a terminator); the rewrite reuses constseq's BUILTIN_STRINGDATA bounded-literal mechanism", "change_kind": "type-inference", "tier": "transform", "symptoms": ["rust or go string argument renders as a raw rodata address beside its length instead of the literal text", "takes_str(&DAT_00102000, 15) where the 15 bytes at that address are plainly the string", "string literals recovered on C binaries but never on rust or go ones"]}, {"option": "gomorestack", "values": ["off", "auto", "always"], "default": "auto", "destructive_as_default": true, "phase": "P2", "subphase": "flow-classification", "strength": "HARD", "rewind": "P2", "issue": "kuna-multilang-gomorestack", "summary": "Fold the Go stack-growth prologue. Every non-leaf function the Go compiler emits opens with a guard - compare SP against the goroutine's stackguard, and on failure call runtime.morestack(_noctxt) and LOOP BACK TO THE FUNCTION ENTRY to re-run the prologue on the grown stack. The check is pure runtime machinery (the function's own semantics are the fall-through path), but the back-edge makes the entry a loop head, so every live argument register joins its incoming value with its after-growth value and parameter recovery sees phi-fed arguments on every non-leaf Go function. The fold matches the tight guard/grow two-block cycle sitting on the inserted entry block (the shared entry-guard-loop shape, infra/idiom.rs) with the cold arm calling a runtime.morestack* routine - the name is only accepted IN the shape, so a coincidental name in straight-line flow cuts nothing - and removes the guard edge pre-SSA (the cleanupcode window), so the entry phi never forms and the guard's compare and stackguard load die to ordinary dead-code elimination. At auto (default) the fold acts only when the image's source language is Go (the sourcelang fact), making it a structural no-op on every non-Go binary; always drops the language test for corpora with no detection signal (a stage-test bytechunk); off keeps the guard for auditing.", "use_when": "Leave auto: on a Go binary the fold turns every function's entry-loop + runtime.morestack_noctxt() noise into straight-line flow and un-breaks parameter recovery; off only to audit the real prologue instructions (e.g. verifying a hand-patched stackguard). Use always on a Go function carved into a bytechunk or a raw image where detection has no buildinfo section to find.", "example": "option gomorestack off", "source_decompiler": "kuna", "inspiration": "The Go compiler's stack-growth prologue (cmd/compile, runtime.morestack_noctxt); the removal primitives are the securitycheck/stackguard pair (removeBranch + removeUnreachableBlocks) moved to the pre-SSA window", "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"]}, + {"option": "goreg", "values": ["on", "off"], "default": "on", "destructive_as_default": 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 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"]}, {"option": "goabi", "values": ["auto", "off"], "default": "auto", "destructive_as_default": false, "phase": "P1", "subphase": "environment-binding", "strength": "HARD", "rewind": "P1", "issue": "kuna-multilang-goabi", "summary": "Select the vendored golang compiler spec for a Go image. Go does not use the platform C ABI: since 1.17 its register ABI (abi-internal) passes arguments in RAX,RBX,RCX,RDI,RSI,R8-R11 on x86-64 and returns multiple values in those same registers, so under the default gcc model a Go function's parameters are invisible (RAX/RBX are scratch to SysV), a second return value is dead-coded away, and the statements computing it are DELETED - `func pair(a,b int) (int,int)` decompiles as `long pair(void){ return v1 + v2; }` with both locals unassigned and the product gone. Ghidra vendors golang cspecs (abi-internal default, abi0 the stack ABI, the write-barrier and duff helper models) for x86-64, x86-32 and AArch64; kuna ships them compiled and nothing selected them. At `auto` (default) an image whose effective source language is Go (the sourcelang fact, KUNA_SOURCELANG force included) composes the `:golang` compiler field in its SLEIGH language id on those three little-endian architectures; every other architecture and every non-Go image is untouched, and the engine's default-model retry covers a spec that fails to resolve. The id is composed inside `load file`, so the option travels as the KUNA_GOABI environment variable (the rexthunk/sourcelang loadtime pattern). The golang cspec also registers the `abi0` stack-ABI model, and a function or callee whose name ends `.abi0` (Go's dual-ABI stack alias - an unstripped go1.22 hello-world carries 115 such .symtab names; the pclntab carries none, so a stripped image keeps abi-internal everywhere) takes that model at the model-selection seams; the binding self-gates on the model being registered, so an explicit `--target ...:golang` gets it even with goabi off.", "use_when": "Leave auto: on a Go binary the golang cspec is the difference between invisible parameters with deleted statements and recovered register arguments with the multi-return join; flip off to read a Go image under the platform C model (comparing against another tool, or triaging a suspected cspec defect). Non-Go binaries never see the option either way.", "example": "option goabi off", "source_decompiler": "kuna", "inspiration": "Ghidra's vendored x86-64-golang/x86-32-golang/AARCH64_golang cspecs (abi-internal, abi0) and its Golang language-id selection; the loadtime env bridge is the rexthunk/sourcelang pattern", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["go function parameters missing or void while the body reads rax/rbx locals that are never assigned", "a go function returning two values loses the second and the statements computing it", "go output full of unassigned register-commented locals", "multi-return go call sites read registers the call never defines"]}, {"option": "gowritebarrier", "values": ["off", "auto", "always"], "default": "auto", "destructive_as_default": true, "phase": "P2", "subphase": "flow-classification", "strength": "HARD", "rewind": "P2", "issue": "kuna-multilang-gowritebarrier", "summary": "Fold the Go GC write-barrier diamond. Every pointer store the garbage collector must see compiles to a flag test: when runtime.writeBarrier is set, call a runtime.gcWriteBarrier thunk (which hands back the GC pointer buffer, R11 on amd64) and record the old and new pointer values in it; both paths then perform the program's own stores at the join. The cold arm is GC bookkeeping with no bearing on what the function computes, and it actively corrupts the output today: the thunk's buffer register is not a modeled return, so the recording stores read an undefined local and the value split can mangle even the real store's address expression. The fold matches the shared guarded-cold-call shape (infra/idiom.rs): a one-block cold arm holding exactly one call, whose guard rejoins the hot path in one hop - the callee name (runtime.gcWriteBarrier, gcWriteBarrier1..8, the bare assembly thunk gcWriteBarrier; never wbBufFlush, which user code cannot call inline) is only accepted INSIDE that shape, so a coincidental name in ordinary flow cuts nothing. The guard edge is removed pre-SSA (the cleanupcode/gomorestack window, registered directly after gomorestack), so the buffer-feeding loads die to ordinary dead-code elimination and the program's stores at the join are untouched. At auto (default) the fold acts only when the image's source language is Go; always drops the language test (stage-test bytechunks); off keeps the barrier for auditing GC behavior.", "use_when": "Leave auto: on a Go binary every guarded pointer store sheds its if (runtime_writeBarrier) { gcWriteBarrierN(); buffer writes } wrapper and renders as the plain store; off only to audit the GC's barrier placement. Use always on a Go function carved into a bytechunk where detection has no buildinfo section to find.", "example": "option gowritebarrier off", "source_decompiler": "kuna", "inspiration": "The Go compiler's GC write barrier (cmd/compile wbcall lowering, runtime.gcWriteBarrier1-8 buffer protocol); the removal primitives are the securitycheck/stackguard pair moved to the pre-SSA window", "change_kind": "structure-recovery", "tier": "transform", "symptoms": ["every pointer store in a go function wrapped in if (writeBarrier flag) with a gcWriteBarrier call", "stores through an undefined local after a gcWriteBarrier call because the thunk's buffer register is not modeled", "go output doubled in size by GC bookkeeping diamonds"]}, {"option": "sourcelang", "values": ["auto", "c", "rust", "go", "swift"], "default": "auto", "destructive_as_default": false, "phase": "P1", "subphase": "environment-binding", "strength": "HARD", "rewind": "P1", "issue": "kuna-multilang-sourcelang", "summary": "Override the detected source language of the image. kuna detects what produced a binary once at load (the port of Ghidra's SourceLanguageAnalyzer: Go build-info sections, rustc .comment records / mangled symbols / .rodata signatures, Swift metadata sections / mangled symbols / runtime imports, else the C toolchain conveniences) and that verdict is a single stored fact consumed everywhere language matters: analysis-pass selection inside `load file` (the Go pclntab pass, the Rust/Go no-return widening), the engine-plane Architecture::source_lang that per-function rules gate on (`rustabi auto`, `reloadarg`'s Go stand-down), and the output-language auto policy. `auto` (default) trusts detection; `c`, `rust`, `go` and `swift` force the verdict for all of those consumers at once. Because pass selection happens inside `load file`, upstream of every per-function option command, the forced value travels as the KUNA_SOURCELANG environment variable set by the CLI before loading (the rexthunk/relocobjects loadtime pattern); the console `option sourcelang` arm updates the engine fact for the already-loaded program and bridges the env for a later `load file` in the same process. Forcing a language never invents facts a binary lacks (forcing `go` on a non-Go image enables the pclntab pass, which then simply finds no table); what it reliably changes is which language-gated rules may act. A `cpp` value is deliberately absent until something consumes a C-vs-C++ distinction.", "use_when": "Force `c` when detection claims the wrong language (a C image embedding Rust-looking strings or a `$s`-prefixed symbol) and language-gated rules are misfiring; force `rust`/`go`/`swift` on a stripped or post-processed binary whose language signals were removed (no .comment record, sections renamed) so the language passes act anyway. Leave `auto` otherwise - detection is high-precision by construction.", "example": "option sourcelang rust", "source_decompiler": "kuna", "inspiration": "Ghidra SourceLanguageAnalyzer / SourceLanguageService.find (detection), plus the kuna loadtime env-bridge pattern (rexthunk, relocobjects)", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["rust binary renders with c output and no rust abi handling because a stripped image lost its language signals", "a c binary is detected as another language and language-gated rules misfire", "go runtime passes do not run on a go binary whose buildinfo section was stripped"]}, diff --git a/docs/baseline-stages.json b/docs/baseline-stages.json index 088fcf163c..a57143fc4d 100644 --- a/docs/baseline-stages.json +++ b/docs/baseline-stages.json @@ -970,7 +970,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: 335 registry rows plus six individual queries", + "data:KUNA-CATALOG #15: 336 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 fa675b9b7d..a148e1b812 100644 --- a/docs/options.md +++ b/docs/options.md @@ -792,6 +792,9 @@ Three tiers: | stripped go binary renders code after a runtime.panicshift or runtime.gopanic call as if the helper returned | [`gonoreturn`](#gonoreturn) | | a go panic arm falls through into the next block and the output interleaves panic and normal paths | [`gonoreturn`](#gonoreturn) | | a go function keeps its morestack entry loop because the grow arm follows a panic call kuna treats as returning | [`gonoreturn`](#gonoreturn) | +| go functions declare a local commented r14 or x28 that is read but never assigned | [`goreg`](#goreg) | +| stackguard reads render as *(long *)(v3 + 0x10) instead of g->stackguard0 | [`goreg`](#goreg) | +| go runtime internals full of unexplained register arithmetic | [`goreg`](#goreg) | | go function parameters missing or void while the body reads rax/rbx locals that are never assigned | [`goabi`](#goabi) | | a go function returning two values loses the second and the statements computing it | [`goabi`](#goabi) | | go output full of unassigned register-commented locals | [`goabi`](#goabi) | @@ -2967,6 +2970,14 @@ Program-prep enablement: what is discovered, decoded, and named before any funct - **Where / provenance:** P1/code-data-partition · kuna · analysis-enablement · kuna-multilang-gonoreturn - **Example:** `option gonoreturn off` +### `goreg` -- on | off, default `on` + +- **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. +- **What it does:** 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. +- **When to flip:** 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. +- **Where / provenance:** P1/environment-binding · kuna · analysis-enablement · kuna-multilang-goreg +- **Example:** `option goreg off` + ### `goabi` -- auto | off, default `auto` - **Symptoms:** go function parameters missing or void while the body reads rax/rbx locals that are never assigned; a go function returning two values loses the second and the statements computing it; go output full of unassigned register-commented locals; multi-return go call sites read registers the call never defines. diff --git a/docs/spec/01-program-prep.md b/docs/spec/01-program-prep.md index 6810ac580f..1b9f8d451a 100644 --- a/docs/spec/01-program-prep.md +++ b/docs/spec/01-program-prep.md @@ -231,6 +231,31 @@ go1.22 pclntab carries no `.abi0` names and the canonical entry inventory collapses the unstripped symtab aliases, so no `.abi0` spelling reaches the engine — alias-aware symbol plumbing has to come first. +(kuna) **With the golang cspec active, the goroutine register becomes the +named, typed global `g`** (option `goreg on|off`, default on, carried as the +`KUNA_GOREG` loadtime environment gate; +`decompiler/crates/kuna-decomp/src/p1_partition/kuna_goreg.rs`, installed by +the architecture build right after the cspec `` decode — +`decompiler/crates/kuna-decomp/src/infra/architecture.rs (kuna_install_goreg)`). +The register ABI pins the current goroutine in a register — `R14` on amd64, +`x28` on arm64, the values the vendored golang `register.info` files state — +which the cspec lists as `unaffected` and no `` names, so every access +rendered as a phantom local (`v3; // r14`) fed by unexplained arithmetic. The +install mirrors a cspec `` (the register's storage joins +the global scope; the vendored spec itself stays pristine) and maps a type- +and name-locked `g` symbol typed `runtime.g *` over it. `runtime.g` and +`runtime.gobuf` are synthesized 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`); the liblink-stated prefix +(`stackguard0` 0x10, `_panic` 0x20, `m` 0x30) is stable across Go releases, +the tail is go1.22-pinned, and an access past the modeled prefix renders as an +offset off `g` — still anchored. A golang target with no pinned register +(x86-32 reaches `g` through TLS) is untouched. Measured on the stripped +go1.22 hello-world: functions declaring a phantom `// r14` local 267 → 23, +functions reading named `g->` fields 7 → 297, the preemption-request store +rendering `g->stackguard0 = 0xfffffffffffffade` exactly as the runtime source +spells it. + The loader's symbol stream — defined FUNC symbols plus the resolved import stubs of §1.3 — is `@VERSION`-stripped, demangled (§1.4) and **character-sanitized** before each diff --git a/tests/stages/kuna-catalog.xml b/tests/stages/kuna-catalog.xml index 9cd26dad7d..374778d564 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"