Skip to content

pclntab: recognize .lrodata_gopclntab and __lrodata_gopcln sections - #1267

Merged
mahaloz merged 16 commits into
Noelo-Lab:mainfrom
zaeku:fix/gopclntab-lrodata
Oct 29, 2026
Merged

mahaloz merged 16 commits into
Noelo-Lab:mainfrom
zaeku:fix/gopclntab-lrodata

Conversation

@zaeku

@zaeku zaeku commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1237.

In large Go binaries built with split/large read-only data linker rules (such as Bazel/Blaze Go toolchains), the pclntab table is emitted in its own .lrodata_gopclntab section rather than .gopclntab or .rodata:

  • ELF: .lrodata_gopclntab
  • Mach-O: __lrodata_gopcln (truncated to 16 bytes by Mach-O's sectname[16] limit)

Because pclntab sits in that dedicated section instead of __gopclntab or __rodata, both step (1) and the step (3) fallback scan in locate_header miss it.

The fix

  • Add ".lrodata_gopclntab" and "__lrodata_gopcln" to the step (1) named-section lookup in locate_header.

Tests

  • pclntab/tests.rs: macho_lrodata_gopcln_section_is_found_and_decoded exercises recover_go_functions on a hand-built arm64 Mach-O carrying __TEXT,__lrodata_gopcln with textStart = 0.
  • cargo test -p kuna-analysis pclntab (18/18) and make check-spec pass.

@zaeku
zaeku force-pushed the fix/gopclntab-lrodata branch from 51b21a1 to 42206ef Compare October 26, 2026 01:06
mahaloz and others added 12 commits October 25, 2026 21:55
…reg, default on with the golang spec) (Noelo-Lab#1276)

At spec build, when the golang cspec is active, the g register (amd64 R14, arm64 x28, per the vendored register.info; x86-32 is TLS and gets none) joins the global scope the way a cspec <global><register/> entry would, with a type+name-locked g symbol typed runtime.g* - vendored specs stay pristine. runtime.g/gobuf shells carry only source-verified fields (release-stable liblink prefix + go1.22-pinned tail); past-shell accesses render as anchored offsets. option goreg on|off default on, inert without goabi by construction. Stripped go1.22 witness: phantom r14 locals 267 -> 23, named g-> readers 7 -> 297, wall +1.1%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoTv7T2Ep5hmbZcyYSMk1g
…tstr Go arm) (Noelo-Lab#1277)

The string-record pass gains a Go registration: on real go1.22 binaries (non-PIE, zero relocs) 480/545 of the witness (ptr,len) headers sit in .data/.noptrdata, so the Go arm widens only the SLOT regions; the target test is unchanged (text in genuinely read-only data) and the rustc arm is byte-identical. Longest-first same-start dedup + the occupied-address guard handle Go substring sharing; registered before gopclntab (appended-last contract); same fatstr gate, no new option. 12-binary corpus: fns with a string literal 164 -> 7,046; wall +1.2%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoTv7T2Ep5hmbZcyYSMk1g
…r entry points (Noelo-Lab#1278)

CI's runner Go is newer still than go1.25: exitsyscall no longer carries the
stackguard0 store there either. Which single function touches the field
shifts release to release, but the preemption protocol guarantees it is
written somewhere among the scheduler entry points, so the e2e now scans six
small unambiguous candidates (exitsyscall, reentersyscall, gopark, execute,
park_m, acquireSudog - all rendered it on go1.22.10 and go1.25.3) and
asserts at least one renders g->stackguard0, with every transcript in the
failure message. unlock2 keeps the g-> and off-arm asserts. Test-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoTv7T2Ep5hmbZcyYSMk1g
… runtime.m shell (Noelo-Lab#1282)

Shells are versioned off the image build-info version (GoLayout tables per release: go1.23 inserted g.syscallbp, go1.25 dropped gobuf.ret), fixing the g shell silently mis-naming fields on newer Go; runtime.m + gsignalStack shells join the builder with source-verified fields, g.m typed runtime.m*. Rides goreg, no new option. go1.22 hello: raw g->m[idx] fns 166 -> 42, ~77 fns render named curg/locks; go1.25: 53 curg readers at shifted offsets (was 10). Self-selecting m-field witness green under go1.22.10 + go1.25.3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoTv7T2Ep5hmbZcyYSMk1g
…obool, default auto) (Noelo-Lab#1283)

Go returns a bool as bare SETcc with unspecified upper bytes; upstream rightly refuses to narrow without zero-evidence (rustc zeroes first), so RETURN(PIECE(stale7,bool1)) rendered CONCAT71 artifacts and 8-byte signatures. RuleGoBoolReturn rewrites the RETURN to ZEXT(bool) when the low byte is a genuine boolean definition; the dead concatenation falls to DCE, the PIECE survives other readers - no deleted stores by construction. gobool off|auto|always, default auto (Go images). Stripped hello: 57 fns change, CONCAT71 fns 64 -> 34 (residue = mid-function stores, separate judgment), type:.eq.* helpers recover bool signatures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoTv7T2Ep5hmbZcyYSMk1g
…ata (option gotypes, default on) (Noelo-Lab#1284)

Moduledata located on stripped images by scanning data sections for a word pointing at the pclntab header (validated by the funcnametab slice beside it); types/etypes + typelinks found by cross-validation (sampled entries must decode) rather than per-release offsets, absorbing go1.18-1.25 drift; typelink pointer forms resolve to their named pointee; _type/name decoders cover both layouts. Phase 1 is a provider: hello 356 typelinks -> 143 named shells, gofmt 731 -> 322, vet 1626 -> 638; output byte-identical on/off across all witness functions by proof; cost below the measurement floor. option gotypes on|off default on, Go images only. E2e pins main.Point end-to-end under go1.22.10 and go1.25.3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoTv7T2Ep5hmbZcyYSMk1g
…e typelinks slice is gone (Noelo-Lab#1285)

go1.26 emits firstmoduledata into a new .go.module section (the anchor scan never looked there) and removed the typelinks slice (runtime builds the list by walking [types+PtrSize, types+typedesclen) with per-kind descriptor strides). Fix stays shape-detected: anchor scan gains the .go.module spellings; meta locator tries the slice form, else the types/typedesclen/etypes triple walked with strides faithful to internal/abi.Type::DescriptorSize, accepted only when >=8 records decode back-to-back. Full Go e2e family green under go1.22.10/1.25.3/1.27.2; suite ran green with 1.27.2 on PATH; proven on CI toolchain via full-ci. Strict fix, no counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoTv7T2Ep5hmbZcyYSMk1g
…nly subset) (Noelo-Lab#1286)

A ported name-only Swift demangler subset (parse-or-passthrough: context chains, functions with argument labels, accessors, init/deinit, six global markers; punycode/witnesses/thunks/specializations pass through byte-identical; the one ambiguous production refused). Wired beside the Rust/Itanium/MSVC demanglers behind the Swift name-shape test; swiftdemangle on|off default on, loadtime env bridge. Crate alternatives rejected on evidence (cmake C++ build + rustc floor; unvetted 0.x). Corpus hand-derived from the Swift ABI mangling spec with per-case citations. E2e synthesizes an ELF with genuine mangled names (no Swift toolchain exists here or on CI - the vendored-fixture follow-up is named in the spec prose).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoTv7T2Ep5hmbZcyYSMk1g
… export (Noelo-Lab#1290)

* Add --stream-layout address: streamed progress over a reproducible export

`decompile-project --stream --stream-layout address` runs the non-stream
export unchanged (same serial or pooled decompile, plan, structure naming,
merge and artifacts) and reports on it while it runs: `.streaming` on its
clock, a running README, and `index.jsonl` as a per-function feed in
completion order with null `.c` spans. At the end the four artifacts are the
non-stream export's byte for byte, `index.jsonl` is replaced by the
address-ordered index of that `.c`, and `.streaming` is removed, so the
folder does not depend on --jobs N wherever the non-stream export does not.

The default layout (`decompile`) is unchanged. Hooks: jobs::PoolWatch on the
pool's planned first pass, a `landed` callback on the serial and
callee-first loops, and decompile_project::ExportWatch around the export.

Measured on /usr/bin/ssh (1,090 functions): address-layout runs at --jobs 2
and 4 and a non-stream --jobs 4 run leave identical folders apart from
index.jsonl; CPU time of the two --jobs 4 arms is within noise.

[AUTOMATED] Authored by an agent.

* Rename --stream-layout address to --progress

Progress reporting and incremental artifacts are two separate things;
`--stream` bundles both, and only the incremental `.c` costs
reproducibility. `--progress` is now its own flag: the status file and the
index.jsonl feed over an ordinary export written at the end. With both
flags the export is the streamed one. `--progress` accepts `--assert`
under the non-stream export's rules. `--stream-layout` is removed.

[AUTOMATED] Authored by an agent.
…ions

The pclntab bullet in docs/spec/01-program-prep.md and locate_header's
header listed only .gopclntab/__gopclntab; add the large read-only data
split's spellings this PR teaches the locator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
@mahaloz

mahaloz commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

[AUTOMATED] Pushed b4d4ef1 (docs only, no code change): the Go pclntab bullet in docs/spec/01-program-prep.md and the locate_header doc header listed only .gopclntab/__gopclntab. They now also name .lrodata_gopclntab/__lrodata_gopcln, which the spec-is-live rule requires. I checked the fix end to end on stripped go1.22/go1.27 ELF and arm64 Mach-O builds whose pclntab section was renamed to these names. Base recovers 0 Go names; the PR recovers the same kuna functions list as the unrenamed binary.

phix33 and others added 3 commits October 29, 2026 00:33
…Lab#1259)

On every PowerPC language, a `lwarx`/`stwcx.` retry loop decompiles as a loop
instead of as one atomic update. In the output below, `v1` is never assigned,
so the store and the loop exit both depend on an uninitialized value.

```sh
printf '\tlwarx 9,0,3\n\taddi 9,9,1\n\tstwcx. 9,0,3\n\tbne- .-12\n\tblr\n' > atomic.s
powerpc-linux-gnu-as atomic.s -o atomic.o
powerpc-linux-gnu-ld -Ttext=0x10000 -e 0x10000 atomic.o -o atomic.elf
kuna decompile atomic.elf 0x10000 --addr
```

```c
void sub_10000(int *a0)
{
  char v1; // reserve
  unsigned char v2; // cr0

  do {
    if (v1) {
      *a0 = storeWordConditionalIndexed(*a0 + 1,0,a0);
      v2 = 2;
    }
  } while (!(bool)(v2 >> 1 & 1));
}
```

With the fix this is `*a0 = storeWordConditionalIndexed(*a0 + 1,0,a0);`.
Compiled `__atomic_fetch_add`, compare-exchange, exchange and spinlock code
shows the same pattern.

## Fix

- `stwcx.` and `stdcx.` skip their store while `RESERVE` is 0, but `lwarx` in
  `ppc_instructions.sinc` had its reservation setup commented out, with a
  misspelled register name. It now sets `RESERVE`, `RESERVE_LENGTH` and
  `RESERVE_ADDRESS`, as the ISA `lwarx` does.
- That constructor fixes the hint bit at 0, so for ordinary encodings it is
  chosen over the ISA one in the ISA languages too. All 23 PowerPC languages
  were affected, and all now produce the single conditional store.

## Tests

- `tests/stages/kuna-ppc-lwarx-reservation.xml`: the loop above on
  `PowerPC:BE:32:default`. It passes 0/2 without the fix and 2/2 with it.
- `kuna-sleigh` `ppc_lwarx_reservation`: `lwarx` writes all three reservation
  registers on generic 32- and 64-bit, e500 and ISA languages. It fails on the
  unpatched specs.
- The datatest corpus is unchanged (675/675). No other stage expectation moved.



Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
…oelo-Lab#1291)

## Problem

A PPC32 call to `__stack_chk_fail` can lose its name and no-return contract, making decompilation walk past the end of the function. With Clang and GNU PowerPC binutils:

```sh
mkdir -p tmp/ppc32-plt
printf '%s\n' 'extern void __stack_chk_fail(void) __attribute__((noreturn));' \
  'int check(int x) { if (x) __stack_chk_fail(); return 7; }' |
  clang --target=powerpc-linux-gnu -O1 -fPIC -fno-unwind-tables \
    -fno-asynchronous-unwind-tables -x c -c - -o tmp/ppc32-plt/check.o
powerpc-linux-gnu-ld -shared --bss-plt tmp/ppc32-plt/check.o -o tmp/ppc32-plt/check.so
kuna decompile tmp/ppc32-plt/check.so check --mode fast
```

```text
error: Unable to load 512 bytes at r0x00000184
```

## Fix

- Resolve BSS-PLT imports from jump-slot relocations and absolute secure-PLT stubs from validated slot addresses.
- Use the imported name for calls, known contracts and xref queries; retain local stub labels as aliases.
- Decline incomplete stubs, conflicting metadata and all unproven `r30`-relative stubs. An absent `.got2` does not prove the caller uses the GOT base.

## Tests

Synthetic ELF32 tests cover both byte orders, stripped and symbolized stubs, returning imports and absent fall-through bytes; the terminal-call test fails before the fix. Collision tests and a GNU-linked fixture with discarded `.got2` require `return 7` to survive.


Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWTixHX2E5cBwneY5aW
@mahaloz
mahaloz merged commit e1f669d into Noelo-Lab:main Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants