Skip to content

MachO: Report a symbol in an instruction-bearing section as a function - #796

Open
zardus wants to merge 3 commits into
masterfrom
feature/machofix
Open

MachO: Report a symbol in an instruction-bearing section as a function#796
zardus wants to merge 3 commits into
masterfrom
feature/machofix

Conversation

@zardus

@zardus zardus commented Aug 27, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

SymbolTableSymbol.is_function returns False unconditionally, so
binary.symbols never names a function on a Mach-O object. On
tests/x86_64/fauxware.macho:

symbols reported as functions: 0 of 14
  _authenticate          0x0100000cc0  section __text    is_function=False
  _main                  0x0100000de0  section __text    is_function=False

In angr that silently empties the CFG's symbol-derived starting points, its
function-boundary fixup, and two protections against dropping recovered
functions.

Root cause

An nlist has no equivalent of ELF's STT_FUNC, so there was nothing obvious
to test; and until #762 MachOSection.is_executable reported __TEXT's
wholesale r-x, so the obvious repair would have called every string literal a
function.

Fix

Now that a section states whether it holds instructions, a symbol is a function
when it is N_SECT, its section holds instructions, and its address lies
inside that section. The containment test is load-bearing:
__mh_execute_header names __TEXT,__text but addresses the Mach-O header, so
without it every linked image seeds the CFG at its own header. On
fauxware.macho that gives 4 of 14 — _authenticate, _accepted, _rejected
and _main — while _sneaky, which lives in __data, stays out.

A second commit normalises ARM's Thumb flag, which Mach-O keeps in n_desc, to
the low address bit the rest of the codebase reads it from. On
tests/armhf/FileProtection-05.armv7.macho, _main moves from 0x83a8 to
0x83a9 while the one ARM-mode definition, stub helpers at 0xa2f0, keeps
its address.

A third commit repairs what the first one breaks in the Go pclntab loader.
register_gopclntab_symbols skipped a pclntab entry whenever any sized function
symbol sat at its address, which was only safe while no Mach-O symbol was ever a
function. With the first commit applied, the Mach-O symbol table covers all 1,888
addresses of aarch64/langdetect_go.macho — but under the platform's leading
underscore, _main.main rather than main.main — so every un-prefixed Go name
disappeared, and angr's language detector matches Go on exactly those names. The
dedupe now compares the entry's name as well as its address, accepting the
.abi0 suffix the Go linker gives an assembly function in the symbol table only.
Seven objects move, out of every object in angr/binaries that has a Go pclntab
(27 of the 894 ELF, PE and Mach-O files under tests/). langdetect_go.macho
goes 0 → 1,888, back to what cle master produces. The other six each gain two
or three symbols, and that is intended: they are Go type-equality thunks whose
name the pclntab abbreviates and the symbol table spells out —
type:.eq.[...]runtime.Frame against type:.eq.[2]runtime.Frame — so the two
names really are different, nothing covers the entry, and the entry is added.
The other twenty objects are unchanged, starling at 6,079 among them. The
full table is in the validation record.

Testing

tests/test_macho.py::test_symbol_is_function and
test_arm_thumb_definition_carries_the_flag_in_its_address cover the first two
commits, on existing fixtures. Both fail on master, where the function count is 0
on each image; on this head it is 4 of 14 on fauxware.macho and 74 of 548 on
the armv7 image.

tests/test_gopclntab.py::TestGoPclntab::test_macho_binary_supplies_the_function_symbols,
which cle master gained in #808, covers the third. Master's own copy of it passes
on master and fails on the first two commits of this branch with
assert 0 == 1888; this branch's copy passes here. The two are not the same
test: this branch adds a block asserting that each of those 1,888 addresses
already carries a function symbol under the _-prefixed or .abi0 spelling,
which is what the address-only dedupe was matching on. That block raises
KeyError on master, where no Mach-O symbol is a function and the map it
indexes is empty, so running this branch's file against master shows an error
rather than a pass.

Validation: #796 (comment)

session: sharpen

@zardus

zardus commented Aug 27, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 42a7cf48bff5a9a990765dd57b93aa02ed234cb6 against baseline 0e77ade3c39a3cee05f65051e57955675e1ac21b, cle origin/master, paired with angr a9c8cbd6 (angr/angr#6984) against angr's own origin/master.

Why the old code was right and is not any more. return False and its comment are original Fraunhofer code from 9b28f31 (2017), sitting beside the same class's other admissions that Mach-O symbol types could not be mapped. It was a deliberate "we cannot know", and correct then: an nlist has no STT_FUNC, and MachOSection.is_executable reported __TEXT's wholesale r-x, so any repair would have called string literals functions. #762 changed that in 2026 by making a section state whether it holds instructions. This is not a bug repair; it is a question that became answerable.

Regressions, failing before and passing after: cle test_symbol_is_function (assert set() == {'_authenticate','_accepted','_main','_rejected'}); cle test_arm_thumb_definition… (assert 33704 == 33705); angr test_symbol_exemption_covers_the_blocks_removal_deletes (AttributeError: 'CFGFast' object has no attribute '_body_names_a_symbol').

The Thumb-flag commit is measured separately: seeding the even address instead of the Thumb-tagged odd one took one fixture from 1 overlapping block to 43.

Suites. cle 239 → 241 passed, zero failures either side. angr 18 failed / 2,494 passed → 1 failed / 2,512 passed; the single shared failure is test_cfgfast_soot::test_invokespecial_on_an_interface, pre-existing against the stale pysoot wheel, and the other seventeen were JVM-init errors in the base instance only. Workspace gate green throughout: environment, workspace, test-inputs, feature-instances, vibr, archinfo, pypcode, pyvex, claripy; angr Rust 35 passed; angr-management 626 passed. Pre-commit clean in both repositories with no SKIP. Lint and type diff against master on all five changed files: no regressions, with pyright reporting non-zero badness so the invocation was live rather than silently empty.

Effect, 34 real-world Mach-O objects of 8 MB or less, scored by the sweep's own checker:

master this pair
aarch64 missed function bytes 283 45
x86_64 missed function bytes 254 0
combined 537 45 (−91.6%)

blocks_outside_executable, blocks_in_data, blocks_overlapping, misaligned_starts and blocks_in_synthetic_memory are 0 on every side, and functions rise by 2,981 while blocks rise by only 1,718 — boundaries being recovered, not code being invented. Attribution between the two changes: cle alone reaches 49 on aarch64, and the angr exemption takes 49 to 45.

Cross-format, 194 public fixtures compared as address sets: 165 identical on all four sets, no ELF fixture differs on any architecture, 20 Mach-O fixtures differ (symbols 0 → 3,839, functions 11,848 → 12,039, blocks 47,481 → 47,484), and one PE fixture differs by a single function and is nondeterministic on master too across five repeats with a byte-identical block set.

Relationship to angr/angr#6861. They overlap rather than compose, and on this sample #6861 is subsumed: base 537, #6861 alone 103, this pair alone 45, both together 45 and byte-identical to this pair alone on functions, blocks and every guard. Both apply cleanly to each other's heads, so the overlap is behavioural, not textual.

Not addressed here. Two objects still fail to load with a pre-existing CLECompatibilityError. And drop_bad_functions has a separate pre-existing defect in its removal loop, deleting by the outer loop's func_addr rather than the block's own address; it is untouched by this change.

Re-keyed 2026-08-29. Re-keyed from 5978baf8f951561ffb0903b0a06d5d64da77bea8 on baseline d2ecea068794d20b1f14d90eecc1bc4bc4cfa431 after a rebase onto c7e0d4db664a45b59c84967ac42d04f82a97b087. git range-diff reports neither commit altered and the branch's diff against its merge base is byte-identical to the one these figures were measured on apart from hunk offsets: master's two commits between the baselines add 20 lines to tests/test_macho.py above this branch's hunks, moving two @@ headers by two lines and changing nothing this branch adds or removes. Every number above therefore describes the same patch on a new base. The angr side of the pair is still quoted at a9c8cbd6, which is the head that measurement was taken on rather than angr/angr#6984's current head. Hosted CI at bd840e300d61332b6f24ae1b1b7cb7dc3ec9ad4d is green: 18 of 18 check runs concluded success (https://github.com/angr/cle/actions/runs/33239295083), and both legacy commit statuses are success.

session: sharpen

Re-keyed 2026-09-04, after a rebase onto cle master 0e77ade3c39a3cee05f65051e57955675e1ac21b. The branch moved from aae361772157ba29e0851882b27ae7f994c63bb0 to 6ac4ad005915bc91be6f0e9d0e9eca806faea484 because angr master bumped its sibling pins from 9.3.4.dev0 to 9.3.5.dev0 on 2026-09-02, and ci / Build installs pyproject.toml with uv pip install --no-sources, so a fresh run on the old head died on a .dev version no index publishes. check-stale-pins.py refuses aae36177 and exits 0 on 6ac4ad00. This was a plain replay: no conflict, no hand resolution, no fixup. git range-diff 929991db..aae36177 0e77ade3..6ac4ad00 marks the commit ! rather than =, and the whole of that difference is the @@ hunk header and neighbouring context in tests/test_macho.py, which come from the new base: @@ tests/test_macho.py: import cle becomes @@ tests/test_macho.py: from cle import MachO. No added or removed line moved -- the two sides' added and removed lines are byte-identical at 108 lines each. Master's own edits to tests/test_macho.py in the range rebased over sit at lines 10-18, 241-282 and 456-483 in new-base coordinates; the only one that comes near this branch's hunks is the import block, where this branch adds from cle.backends.macho.symbol import SYMBOL_TYPE_SECT, SymbolTableSymbol beside imports master rewrote. So every figure above describes the same patch on a new base. The opening line named bd840e300d61332b6f24ae1b1b7cb7dc3ec9ad4d before this edit, which is one rebase further back than the head this pass moved: the branch had already been replayed once without the record being re-keyed. Measured the same way over the whole chain, the added and removed lines at bd840e30 are byte-identical to those at the new head, 108 on each side, so the record and the branch have never described different patches.

Hosted CI at 6ac4ad005915bc91be6f0e9d0e9eca806faea484, read 2026-09-04T16:24Z: 20 checks -- 16 success, 3 failure, 1 cancelled. ci / Build, ci / Lint and ci / Typecheck are green. ci / Test (1), Test macos-15 and Test (Pyodide) each fail on one test, and it is a real interaction with current cle master rather than infrastructure -- the log says Checking out angr/binaries at master, so every fixture is present:

tests/test_gopclntab.py::TestGoPclntab::test_macho_binary_supplies_the_function_symbols
>       assert len(go_symbols) == 1888
E       assert 0 == 1888

That test arrived in cle master b6ff025b (#808) on 2026-09-03, and its own comment states the premise it rests on: "cle reports no Mach-O symbol as a function, so nothing here covers a pclntab address and every entry is added". This change is what makes that premise false -- a symbol-table symbol in an instruction section now reports as a function -- so the pclntab loader finds those addresses already covered and adds no GoSymbol. Test windows-2022 is cancelled rather than failed, the matrix having no fail-fast: false.

The Effect and Cross-format tables above were measured on a cle master that
predates #808 and #810. Both change what CFGFast is handed on Mach-O and PE --
#808 adds GoSymbols from the Go pclntab, #810 publishes LC_FUNCTION_STARTS
as function hints -- so those two tables describe the older base and have not
been re-measured on 0e77ade3.

The same staleness applies to two other artifacts. angr/angr#6984's body says
"the exemption never fires on any of the 22 Mach-O fixtures" and "it fires on two
functions" across 716 ELF and PE fixtures, both measured on that same older cle.
And this pull request's second output comment (2026-08-29) measures master
eac0e554 against head bd840e30, neither of which contains #808 or #810; its
CFG figures -- seed set 0 -> 3,631, recovered functions 7,698 -> 7,994 -- are the
ones #810's function hints move.

Re-keyed 2026-09-04 for head 42a7cf48bff5a9a990765dd57b93aa02ed234cb6, baseline unchanged at 0e77ade3c39a3cee05f65051e57955675e1ac21b. One new commit, 42a7cf48 "Go: Cover a pclntab entry by name as well as address", which repairs the CI failure recorded above rather than restating it. register_gopclntab_symbols deduped on address alone; a Mach-O SymbolTableSymbol carries a placeholder size of arch.bytes, so once this branch reports one as a function the whole pclntab reads as covered. The dedupe now compares the entry's name too, accepting the .abi0 suffix the Go linker gives an assembly function in the symbol table only, and not accepting Mach-O's leading underscore -- which is the point, since angr's language detector matches Go on the un-prefixed names.

Measured over every object in angr/binaries that has a Go pclntab, not only the Go fixtures this branch's tests name. Each of the 894 ELF, PE and Mach-O files under tests/ at angr/binaries 003e82a2 was loaded with cle.Loader(path, auto_load_libs=False) on all three revisions; cle parses a pclntab from 27 of them, and seven of those change. GoSymbols added:

fixture master 0e77ade3 base 6ac4ad00 head 42a7cf48
aarch64/langdetect_go.macho 1,888 0 1,888
aarch64/langdetect_go_go1.18.10 0 0 2
aarch64/langdetect_go_go1.20.14 0 0 3
armel/langdetect_go_go1.18.10 0 0 2
armel/langdetect_go_go1.20.14 0 0 3
i386/langdetect_go_go1.18.10 0 0 2
i386/langdetect_go_go1.20.14 0 0 3

The other twenty are identical on all three revisions, x86_64/starling at 6,079, x86_64/windows/langdetect_go.exe at 1,875, x86_64/windows/131252a8... at 1,821, and the ELF langdetect_go builds at 0.

The Mach-O row is the repair: it moves back to what cle master produces. The six ELF rows are new duplicates and are intended. Each is a Go type-equality thunk whose name the pclntab abbreviates and the symbol table spells out -- type:.eq.[...]runtime.Frame in the pclntab against type:.eq.[2]runtime.Frame in .symtab, at the same address, and likewise type:.eq.[...]internal/cpu.option and type:.eq.sync/atomic.Pointer[...]. The two names really are different, so no symbol covers the entry under the rule this commit states, and the entry is added. Tolerating the elision would mean guessing which spelling the symbol table used; three duplicate compiler-generated thunks on a 1,500-entry table is the cheaper side of that trade, and it is the direction that cannot lose a name.

Both clauses of the comparison are load-bearing: dropping .abi0 adds 115 duplicates on langdetect_go and 119 on langdetect_go_dyn, failing test_clean_binary_does_not_duplicate_symbols; accepting the leading underscore restores the Mach-O loss. On the PE fixture all 23 covering COFF symbols match by name as well as by address, so 1875 is unchanged for the right reason rather than by luck.

Local suite, cle only: 6ac4ad00 1 failed, 262 passed, 9 skipped; 42a7cf48 263 passed, 9 skipped, nothing failed. The one failure on the base is test_macho_binary_supplies_the_function_symbols, which is what this commit repairs. Both arms ran python -m pytest tests -q from a tree extracted with git archive at that revision, with cle on PYTHONPATH, no rebuild, and angr/binaries at origin/master 003e82a2. The base line matches hosted Test macos-15 on the published head exactly.

Hosted CI at 42a7cf48bff5a9a990765dd57b93aa02ed234cb6, read 2026-09-04T19:37:55Z: 20 checks, all 20 green -- 18 check runs success and both legacy commit statuses success (https://github.com/angr/cle/actions/runs/33909522094). The three that failed on the base pass here, and the one that was cancelled ran: ci / Test (1) logs PASSED src/cle/tests/test_gopclntab.py::TestGoPclntab::test_macho_binary_supplies_the_function_symbols; Test macos-15 reports 263 passed, 9 skipped, against 1 failed, 262 passed, 9 skipped on the base, which is the local pair exactly; Test (Pyodide) and Test windows-2022 both pass. ci / Publish Unit Tests Results, the check that aggregates the ten shards, is success.

@angr-bot

Copy link
Copy Markdown
Member

Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_796

@zardus

zardus commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Which Mach-O symbol-table symbols are reported as functions, before and after
this change, on two existing fixtures. The command is the same on both sides:

import cle
from cle.backends.macho.symbol import SymbolTableSymbol
ld = cle.Loader(path, auto_load_libs=False)
m = ld.main_object
for s in m.symbols:
    if isinstance(s, SymbolTableSymbol):
        sec = m.find_section_containing(s.rebased_addr)
        print(s.name, hex(s.rebased_addr), sec.name if sec else "-", s.is_function)

Before — no symbol on any Mach-O object is a function, so nothing
downstream has a symbol-derived function start to work from:

angr/cle master (a4fb800)
tests/x86_64/fauxware.macho
symbols reported as functions: 0 of 14

  ___stack_chk_fail      0x0100000000  section -         is_function=False
  ___stack_chk_guard     0x0100000000  section -         is_function=False
  __mh_execute_header    0x0100000000  section -         is_function=False
  _exit                  0x0100000000  section -         is_function=False
  _open                  0x0100000000  section -         is_function=False
  _printf                0x0100000000  section -         is_function=False
  _read                  0x0100000000  section -         is_function=False
  _strcmp                0x0100000000  section -         is_function=False
  dyld_stub_binder       0x0100000000  section -         is_function=False
  _authenticate          0x0100000cc0  section __text    is_function=False
  _accepted              0x0100000d80  section __text    is_function=False
  _rejected              0x0100000db0  section __text    is_function=False
  _main                  0x0100000de0  section __text    is_function=False
  _sneaky                0x0100001048  section __data    is_function=False

tests/armhf/FileProtection-05.armv7.macho
arch ARMEL   symbols reported as functions: 0 of 548
  '_main'          addr 0x83a8  is_function=False
  ' stub helpers'  addr 0xa2f0  is_function=False

After — the four definitions inside an instruction-bearing section are
functions, the header alias and the __data symbol are not, and the Thumb
definition carries the flag in its address:

with this change
tests/x86_64/fauxware.macho
symbols reported as functions: 4 of 14

  ___stack_chk_fail      0x0100000000  section -         is_function=False
  ___stack_chk_guard     0x0100000000  section -         is_function=False
  __mh_execute_header    0x0100000000  section -         is_function=False
  _exit                  0x0100000000  section -         is_function=False
  _open                  0x0100000000  section -         is_function=False
  _printf                0x0100000000  section -         is_function=False
  _read                  0x0100000000  section -         is_function=False
  _strcmp                0x0100000000  section -         is_function=False
  dyld_stub_binder       0x0100000000  section -         is_function=False
  _authenticate          0x0100000cc0  section __text    is_function=True
  _accepted              0x0100000d80  section __text    is_function=True
  _rejected              0x0100000db0  section __text    is_function=True
  _main                  0x0100000de0  section __text    is_function=True
  _sneaky                0x0100001048  section __data    is_function=False

tests/armhf/FileProtection-05.armv7.macho
arch ARMEL   symbols reported as functions: 74 of 548
  '_main'          addr 0x83a9  is_function=True
  ' stub helpers'  addr 0xa2f0  is_function=True

Corrected 2026-09-04: this comment first gave the denominator as 211, the number
of distinct symbol names. The command above iterates m.symbols, which on that
image is 548 nlist entries; 548 is also the figure angr/angr#6984 and this pull
request's description use. The numerators, 0 and 74, are unchanged.

@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

How much of a corpus's analysis surface this changes, measured rather than
argued. This change does not make anything load that did not load before, so a
load-depth probe reports zero for it; what it changes is what CFGFast is given
to start from, and that is measurable.

Sample. 12,000 objects drawn uniformly at random, from a seeded permutation,
out of a 624,920-object internal corpus of compiler- and vendor-produced
binaries; 11,989 were retrievable and probed. The measurement below uses a
seeded random 250 of the Mach-O objects that already load — 189 thin Mach-O and
61 Universal 2 — carrying 156,423 symbols between them.

Method. Each object is loaded with the catalogue's declared load recipe
against master (eac0e5540516b9199dd6a91933e80dc774ea3eac) and against this
branch's head (bd840e300d61332b6f24ae1b1b7cb7dc3ec9ad4d) in one environment.
The symbol count is a load only; a seeded 40 of the 250 additionally run a
complete CFGFast(normalize=True, resolve_indirect_jumps=True) on both sides,
of which 38 finish inside 180 s on both.

Before. Of those 156,423 Mach-O symbols, master calls 0 of them
functions. is_function on SymbolTableSymbol returns False unconditionally.
That matters beyond the property itself, because
CFGBase._load_func_addrs_from_symbols is {sym.rebased_addr for sym in binary.symbols if sym.is_function} and CFGFast unions that into its starting
points — so on Mach-O, the symbol table contributes no function starts at all.

After. 86,800 of the 156,423 symbols (55.5%) are classified as functions,
and 249 of the 250 objects gain some. 1,014 of them get the Thumb bit set on
32-bit ARM. On the 38 that run a full CFG, the seed set goes from 0 to 3,631
addresses, recovered functions from 7,698 to 7,994 (+3.8%) — up on 5
objects, down on none — and total analysis time from 260 s to 204 s.

What that +296 is, stated precisely. CFG nodes go from 53,861 to 53,864, a
change of 3. So this is not 296 newly discovered pieces of code: it is very
nearly the same blocks, attributed to more and better-delimited functions
because the symbol table now says where the boundaries are. That is the honest
description of the win, and it is the win that matters downstream, where
function extents decide what a decompiler is handed.

Control. In a separate 1,241-object subset covering every failure class in
the sample plus 722 objects that already reached CFG, this head changes no
outcome — 0 of 722 control objects differ, and no failure class moves.

The corpus is not redistributable, so its objects are described by architecture,
format and OS rather than named.

session: sharpen

`SymbolTableSymbol.is_function` returned False for every symbol in every
Mach-O object, under the comment "It is not possible to decide wether a
symbol is a function or not for MachOSymbols".

That was not an oversight. It arrived with the original Fraunhofer
contribution in 9b28f31, next to the class's other admissions -- "the
symbol type of all symbols is SymbolType.TYPE_OTHER because without docs
I was unable to properly map Mach-O symbol types to CLE's notion of a
symbol type", "most stock functionality from Angr and related libraries
WILL NOT WORK PROPERLY on MachOSymbol" -- and on its own terms it was
right: an `nlist` has no equivalent of ELF's `STT_FUNC`. `n_type` states
where a symbol is defined, never what it defines, so the symbol table
alone cannot answer the question.

What can answer it is the section. A Mach-O states in each section's own
attributes whether that section holds machine instructions, and until
#762 CLE could not read that back: `MachOSection.is_executable` returned
the parent segment's permissions, and ld64 marks the whole of `__TEXT`
r-x, so a string literal was as executable as the code beside it. Now
that a section answers for itself, a symbol defined in one that holds
instructions names code, and a symbol defined anywhere else does not.

The predicate is that, and the two filters it needs:

* Only an `N_SECT` symbol defines anything in this object at all. An
  undefined, common, prebound or indirect symbol names something another
  object defines, an `N_ABS` symbol names a constant, and a debug entry
  keeps its own type in `n_type`, which `sym_type` returns unmasked, so
  no `N_STAB` entry reaches the section test.
* The address has to lie inside the section the symbol names.
  `__mh_execute_header` is an `N_SECT` symbol on the first section of
  `__TEXT` whose value addresses the Mach-O header in front of it; every
  linked image has one, and without this test every one of them would
  hand angr the file header as a function start.

The new `section` property is what both tests read, and it is bounds
checked: `segment_name` and `section_name` index `sections_by_ordinal`
directly and raise `IndexError` on an ordinal the object does not have.
ARM keeps the Thumb flag of a definition in `n_desc` and leaves `n_value`
even. ELF states the same thing in bit 0 of `st_value`, and that is where
the rest of the stack reads it -- angr's CFG decides the decoding mode of a
starting point from the low bit of the address.

So an image whose symbols the previous commit made visible handed the CFG
even addresses for Thumb code and it decoded them as ARM. On the ARMv7
FileProtection fixture, 73 of 74 function symbols are Thumb definitions, and
seeding them unmarked took the CFG from 1 overlapping block to 43 -- blocks
decoded twice, in two modes, from the same bytes. Normalising the address
takes that back to 1, the same as before any symbol was reported, while the
recovered functions still rise from 277 to 283 and the blocks from 755 to 758.
register_gopclntab_symbols skipped a pclntab entry whenever any sized function
symbol sat at its address. That was safe only while no Mach-O symbol was ever
reported as a function. Once the earlier commit in this branch reports one, the
Mach-O symbol table covers all 1,888 addresses of the Go fixture under the
platform's leading underscore -- _main.main, not main.main -- and every Go name
disappears. angr's language detector matches Go by the un-prefixed names, so a
Go Mach-O stops being detected as Go at all.

Compare the name too. A symbol covers an entry when it carries the entry's own
name, or that name with the ".abi0" suffix the Go linker gives an assembly
function in the symbol table only. An entry with no name keeps the old
address-only rule, since there is nothing to compare.

Measured over every object in angr/binaries that has a Go pclntab -- 27 of the
894 ELF, PE and Mach-O files under tests/ -- seven change. GoSymbols added:
aarch64/langdetect_go.macho 0 -> 1888, which is what cle master produces; and
{aarch64,armel,i386}/langdetect_go_go1.18.10 0 -> 2 with the matching
go1.20.14 builds 0 -> 3. The other twenty are unchanged, x86_64/starling at
6079 among them.

The six ELF ones are new duplicates and they are intended. Each is a Go
type-equality thunk whose name the pclntab abbreviates and the symbol table
spells out: "type:.eq.[...]runtime.Frame" against "type:.eq.[2]runtime.Frame",
at the same address. The two names really are different, so nothing covers the
entry under the rule above and the entry is added.
Tolerating the elision would mean guessing which spelling the symbol table
used, and would trade a lost name for a duplicate one.

Both clauses are load-bearing. Dropping the ".abi0" clause adds 115 duplicates
on langdetect_go and 119 on langdetect_go_dyn; accepting the leading underscore
keeps the Mach-O loss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4CH4T6AJhq6vxurNpzyxU
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.

2 participants