Files
reverse-engineering-anything/docs/specs/native/native-initializers/spec.md
T
8bc01b48b4 feat(native): list ELF and Mach-O initializer and finalizer functions (#9574)
* feat(native): list ELF and Mach-O initializer and finalizer functions

inspect_native_initializers and inspect-native-initializers list the
functions an ELF or Mach-O image runs before and after main: ELF
DT_INIT, DT_FINI, preinit, init and fini arrays and .ctors/.dtors, and
Mach-O __mod_init_func, __mod_term_func and __init_offsets, with slot
addresses, resolved targets, symbols where available and relocation
facts for relocatable objects. Nothing is executed. Slot, symbol,
relocation and note budgets are shared across tables, sentinels are
matched at the image width, and target-derived names are bounded and
marked untrusted.

* fix(native): keep ELF initializer slots unresolved when relocation coverage is partial

A skipped, unreadable or over-budget section relocation table now keeps
every slot it could reach unresolved instead of reading the stored word as
raw or dropping a .ctors slot as a sentinel. Dynamic writes that only
partly overlap a slot, or write it twice, leave it unresolved. Linked
images no longer reapply retained non-SHF_ALLOC link-time records.

The dynamic relocation walk now pauses at the shared reader's step
boundaries and observes cancellation there, resolved targets need only
their first byte mapped, and unreadable symbol names make the report
incomplete.

* fix(native): stop reporting unchecked Mach-O initializer words as resolved

Object files now apply their own section relocations: an UNSIGNED entry
against a defined symbol gives symbol_addend, one against an undefined
symbol gives unresolved_import, and any other entry leaves the slot
unresolved. Linked images without a fixup decoder, such as 32-bit ones,
keep pointer slots unresolved when an LC_DYLD_INFO bind stream or
LC_DYSYMTAB external relocations could replace the stored word.

LC_DYLD_INFO rebases now report dyld_info_rebase instead of
chained_fixup, and unreadable symbol names make the report incomplete.
Section headers keep their declared reloff and nreloc for this.

* docs(native): move initializer guide under docs/guides/native

The documentation guide assigns analyst workflows to docs/guides/<domain>/.
Move the guide there, place its navigation and directory entries in the
Native binaries group, and update links from the docs index, CLI guide,
spec and authored skill. The public /native-initializers route is
unchanged.

The guide and spec also describe per-slot relocation coverage, retained
link-time records, Mach-O object relocations, undecoded bind streams,
the dyld_info_rebase resolution and unreadable names.

* fix(native): keep IFUNC and partly relocated ELF initializer slots unresolved

An absolute relocation against an STT_GNU_IFUNC symbol no longer reports
the resolver address as the target. Section relocation records now use
their real write width, so a record that writes only part of a slot,
starts inside it or repeats it leaves the slot unresolved instead of
reading a raw zero or dropping a .ctors slot as a sentinel.

On 32-bit ARM the Thumb bit is cleared when locating a target's section
and file offset, while value_va keeps the stored callable value.

* fix(native): locate Thumb Mach-O initializer targets and reject arm64_32 with a 64-bit header

On CPU_TYPE_ARM the Thumb bit is cleared when locating and naming a
target, since nlist values do not carry it. An arm64_32 image that
declares a 64-bit header is rejected as malformed rather than read with
8-byte slots. The guide and spec describe IFUNC relocations, partly
relocated section slots, Thumb targets and this rejection.

* fix(native): index initializer address lookups and keep ambiguous mappings unknown

ELF and Mach-O initializer lookups now build the shared file mapping
index once per image, so each slot and target lookup is logarithmic in
the number of load segments rather than a scan of every mapping.

An address that load segments map ambiguously no longer falls back to a
section header offset. Its file offset is reported as unknown and the
report is incomplete; only genuinely unmapped spans use the section
fallback.

* fix(native): report Mach-O bind names over the name budget

A bound import name longer than the 1024-byte name budget was dropped
without a trace, although it is the only identity of an unresolved
initializer target. It now counts toward the over-long name limitation
and makes the report incomplete.

* test(native): count mapping lookups instead of timing the load index regression

The load-mapping regression asserted a wall-clock bound, which is
sensitive to machine load. It now spies on the shared mapping module and
requires the same number of mapping scans and index builds for 8 and 64
slots, so a per-slot scan of every load segment fails deterministically
on a small fixture.

* fix(native): yield while decoding legacy rebase runs

* fix(native): preserve initializer mapping and coverage

* fix(native): index legacy fixup spans

* fix(native): preserve relocation uncertainty and bound bind lookup work

* fix(native): validate ELF slot mappings and yield section relocation scans

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-09 08:58:12 -05:00

91 lines
5.5 KiB
Markdown

# Native initializer and finalizer functions
## Problem and outcome
Analysts reviewing a native image need the functions it declares to run before
`main` and at exit, without executing it or opening a disassembler provider.
`inspect_native_initializers` / `inspect-native-initializers` lists the
initializer and finalizer slots of one local ELF or Mach-O image, with each
slot's location, resolved target and how that target was obtained. The
[user guide](../../../guides/native/native-initializers.md) documents the result.
## Requirements
- Accept one local ELF32, ELF64, thin Mach-O or universal Mach-O image. A
universal Mach-O requires `architecture` to select one slice; other inputs
reject it. The CLI resolves relative paths; MCP requires an absolute path.
- ELF sources, in order: `DT_PREINIT_ARRAY`, `DT_INIT`, `DT_INIT_ARRAY`, legacy
`.ctors`, `DT_FINI`, `DT_FINI_ARRAY`, legacy `.dtors`. Without the dynamic tag,
fall back to `SHT_*_ARRAY` sections or `.init_array*` / `.fini_array*` /
`.preinit_array` names. Mach-O sources are sections typed
`S_MOD_INIT_FUNC_POINTERS`, `S_INIT_FUNC_OFFSETS` and
`S_MOD_TERM_FUNC_POINTERS`.
- Resolve values through RELATIVE and RELR dynamic relocations, then section
REL / RELA records for slots no dynamic relocation covers, then the stored
word when relocation coverage is known. Mach-O uses chained-fixup rebases,
`LC_DYLD_INFO` rebases (reported separately), `MH_OBJECT` section
relocations and `__init_offsets` image-base offsets. Anything else is
`unresolved`, never a stored zero.
- Relocation coverage is per slot. A slot that a dynamic or section relocation
only partly overwrites or writes more than once is `unresolved`, and so is an
absolute relocation against an `STT_GNU_IFUNC` resolver. On 32-bit ARM the
Thumb bit is cleared for target section and file-offset lookup only. A slot with no
relocation found is `unresolved` when a dynamic table, or a section table
that could reach it, was not fully read; such a `.ctors` slot is not dropped
as a sentinel. A linked image's non-`SHF_ALLOC` relocation sections were
applied at link time and are not reapplied. A Mach-O stored pointer word is
used only when no undecoded bind stream or external relocation could
replace it.
- Relocatable objects (`ET_REL`) report section-relative offsets and say so.
- `.ctors` / `.dtors` sentinels are 0 and all ones at the image's pointer width;
they are omitted and counted in a limitation.
- `DT_INIT` / `DT_FINI` have no slot pointer: their slot is the dynamic value
field, and `slot_va` / `slot_section` are null when that field is outside
every load segment.
- Names are exact untrusted file text marked `observed-untrusted`. A target is
named only by a defined symbol whose value equals it, or by the import name
of an unresolved symbolic relocation or chained bind. An unreadable name
(missing string table or offset outside it) is reported as no name and makes
the report `incomplete`.
- Bounds: 64 MiB default file budget (512 MiB maximum); 65536 slots in total;
65536 symbols across all ELF symbol tables; 65536 symbols across all
Mach-O `LC_SYMTAB` commands; 1048576 section relocation records across all
relocation sections; 1024-byte names. Reaching a bound makes the report
`incomplete` with a limitation.
- Legacy Mach-O bind lookup permits at most 4096 candidate runs per query
and twice the file byte count across image queries. Exhaustion leaves
affected pointers unresolved; stream parsing and slot batches yield.
- Limitations are bounded: at most 24 distinct image observations, repeated
per-row reasons reported once with a count, and at most 40 lines in total.
- Section lookups for slot and target addresses use an index built once per
image, so their cost does not grow with headers times slots.
- Paging uses `offset` / `limit` (default 64, maximum 4096) over a stable
ordered index. `coverage.examined_entries` counts every slot read, including
omitted sentinels; `total_entries` is set only when the inventory is complete.
- Malformed headers, multiple `PT_DYNAMIC` segments and truncated dynamic
sections fail without partial success. Cancellation is honored during slot
collection, the section relocation walk and the dynamic relocation walk,
which yields to other work at the shared reader's step boundaries.
## Acceptance
Parser tests cover ELF64 PIE (RELA, RELR, imports, `DT_INIT` / `DT_FINI`),
ELF32 REL in-place addends, `ET_REL` section relocations and sentinels,
unsupported relocation machines, slot, symbol and relocation budgets,
over-long names, tallied repeated observations, a near-maximal section table
with a full slot budget, cancellation in the relocation walks, skipped section
relocation tables, partly overwritten slots, retained link-time relocations,
unreadable names, Mach-O chained fixups, `LC_DYLD_INFO` rebases, 32-bit images
with undecoded binds, `MH_OBJECT` section relocations, `__init_offsets`, the
shared and repeated `LC_SYMTAB` budget, and universal slices. CLI acceptance, filesystem boundary
and MCP boundary tests exercise the public workflows and Evidence output.
## Out of scope
Executing the image or proving that a listed address runs; runtime ordering
(file order is reported); disassembling initializer bodies; PE/COFF TLS
callbacks; Android packed relocations and machines other than x86-64, i386,
AArch64 and 32-bit ARM for relocation decoding; Mach-O fixup decoding for
images other than little-endian 64-bit arm64 and x86-64, and legacy
`LC_DYSYMTAB` external relocations.