* fix(targets): select a preferred Mach-O host subtype
Host admission refused every extra slice of the host CPU type, so arm64
plus arm64e and arm64e ABI variants never became universal targets. Rank
known subtypes and keep the earliest capability variant. Identical
subtypes and ties between unlisted bases still fail closed.
* fix(artifacts): treat a one-sided declared digest as metadata
An all-zero Godot MD5 was visible only because limitation text differed.
After comparison stopped using that prose, a claim on one side and no
claim on the other lost its metadata dimension. Compare the structured
integrity claim instead.
* fix(browser): keep unavailable resource inventories unknown
Directional coverage treated a resource retained from an invalid protocol
tree as an addition against a complete peer. An unavailable collection is
not a truncated sample, so it cannot prove membership.
* fix(native): read the admitted Mach-O slice for metadata
Capability-variant admission chose the first slice, then dispatch
metadata and slice tools ranked the FAT file again and rejected that
pair. Pass the admitted offset, and refuse a winner set that repeats
a subtype.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* fix(native): keep inspect_macho usable when dyld_info refuses the slice
dyld_info exits 1 with 'unknown platform' for Mach-O files without a
platform load command and with 'old arm64e bind opcodes not supported'
for pre-ABI arm64e binaries. inspect_macho then failed outright with the
generic execution_failure remediation although nm, otool and the load
table still describe the slice. Treat these two refusals like the
missing-UUID refusal: imports are reported unavailable, exports keep the
nm observations, and the dyld_info diagnostic is recorded as a limitation.
* test(native): cover dyld refusal fallback and limitations
* fix(native): preserve Mach-O target selection in macOS tools
* fix(native): retain exact Mach-O slice signature coverage
* fix(native): admit x86_64 LIB64 signature selectors
* test(native): match signature checks to actual slices
* fix(native): recognize vtool refusals on private Mach-O aliases
Match vtool's container refusal to the operand the command received so a
parenthesized universal path keeps completed facts and the refusal
limitation. Expect codesign -a only when the target format is universal.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* refactor(macho): extract non-throwing container and slice load-command readers
Split readMachoImage into readMachoContainer, which classifies a file as
thin, FAT or not Mach-O and keeps FAT range issues per slice, and
readSliceLoadCommands, which reads one slice header and frames its load
commands, returning every command framed before a failure.
readMachoImage keeps its strict policy on top of them: commands framed
before a framing defect still decode first, an unsupported FAT slice
still yields to a later malformed one, and the first FAT range issue is
still reported. A characterization test pins that precedence for inputs
with several defects; it passes against both the previous and the new
implementation.
Part of #4306.
* feat(macho): report per-slice encryption status in inspect_macho
inspect_macho now reports the LC_ENCRYPTION_INFO(_64) state of every
slice in a new `encryption` collection, read in process from load
commands without reading code bytes.
- Each slice is encrypted, not-encrypted, absent, malformed or unknown.
Absent requires a completed load-command walk; malformed and unknown
carry a reason and keep commands read before the failure.
- The named range is end-exclusive in slice and input-file coordinates
and maps to file-backed segment fragments, VM addresses and the
overlapping nonempty sections.
- A ciphertext range adds a limitation naming its input-file bytes.
- The in-process read and the native tools are bracketed by one regular
file descriptor and stat identity checks; a change reports
artifact_changed. A missing or inaccessible target keeps the tool
facts and reports encryption as non-exhaustive, never absent.
- Retained Evidence recorded before this field is parsed by the owning
schema and reports encryption as unknown in the macho view and in
managed/native verification.
Fixes#4306
* fix(macho): keep slice facts and failures under large tables and close errors
- Frame load commands through a visitor instead of retaining every
command, restoring the pre-refactor memory profile (2M eight-byte
commands: peak heap 370 MiB -> 0.9 MiB).
- Confirm a large FAT container from a valid slice's Mach-O header so a
sibling's range fault stays per slice instead of hiding healthy slices.
- Name only sections whose bytes the range and their segment both cover.
- Throw inspection failures inside the owned descriptor callback so a
close failure keeps them as its primary error.
* fix(macho): map encrypted ranges only within segment VM extents
mapRange gave every file-backed segment fragment VM addresses, even for a
segment with no VM size, whose file bytes dyld never maps, and past vmsize
when a segment declared more file bytes than memory.
Read vmsize with the segment. A segment with no VM size keeps its fragment
with null VM addresses. A file size beyond the VM size, or a VM range past
the address space, makes the mapping unknown with a reason while the slice
keeps its encryption status.
The docs also claimed the encryption read never reads code bytes. Like
every Mach-O container read, a 0xcafebabe input of at most 16 MiB is read
whole to rule out a JVM class file; say so.
* fix(macho): read encryption through the admitted descriptor facts
Main's withArtifactRegularFile now hands callers the admitted handle and
its initial stats. Use those facts instead of re-stating the handle, and
pass the legacy Evidence payload straight to createEvidence.
* fix(macho): retain a finished report when the target close fails
The file owner keeps the completed callback value when the descriptor close
fails or cancellation arrives during close. inspect_macho now validates that
value and retains the report as the error's partial observation, as
inspect_signature does, instead of returning only the cleanup or
cancellation error.
* test(native): supply readEncryption in merged dyld refusal cases
main's new dyld refusal cases build NativeMachoInspectionContext without
the encryption reader this branch requires.
* test(views): bind the Mach-O encryption page to its facet
main's facet-ownership case builds inspect_macho Evidence without the
encryption coverage this branch requires; the fixture now carries one
slice row and the loop checks the encryption page like the other facets.
* fix(macho): bound encryption commands and range mapping across slices
A slice retains at most two encryption load commands, since a second one
already makes it malformed; the reason keeps the full count. Range mapping
in every slice shares one budget of segment and section records, charged
from each segment command's capacity before it is decoded. A slice past
the budget keeps its encryption status and reports an unknown mapping.
* style(macho): format encryption reader and binding test
* fix(macho): stop retaining segments past the mapping budget
Segment commands are collected during framing only while their mapping
records still fit in the budget shared by all slices, and the budget is
charged only when the slice maps its range.
* docs(macho): clarify retained mapping allowance
* fix(ci): own the Mach-O encryption test in Apple scope
---------
Co-authored-by: t <t@t>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Distinguish omitted slice fields from declarations that cannot be interpreted, and preserve their source in native results and artifact limitations. Keep structural fallback and capacity failures explicit.
Parse literal import framing and retain ambiguous boundaries. Consult undefined symbol-table names only when needed to distinguish producer spacing, with import and export captures owned separately.
Validation: 59 native tests passed and real Apple tools on inert arm64 fixtures reproduced the old loss and verified the fix. Fresh build, typecheck and lint passed. Small warm-task median elapsed was unchanged; cold costs and agent tokens were not measured.
inspect_macho reported only the symbol-table spelling (`_main`), while
LLDB, dlsym and observe_native_calls use the runtime spelling (`main`).
Passing a static name as a breakpoint silently selected a different
function, so results from the two tools could not be joined by name.
Keep `name` as the exact symbol-table fact and add a derived
`runtime_name` to every export and import row, produced by one shared
Mach-O symbol-name owner. Managed P/Invoke verification now uses the
same owner instead of its own underscore stripping.
Fixes#5298
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Keep the exact producer expression and report unknown symbol identity when chained-import text cannot distinguish a binding addend from a literal name suffix.
Fixes#6648
BREAKING CHANGE: inspect_macho import names can be null when identity is ambiguous. Use raw_name for the observed expression and name_status to distinguish it from an exact symbol name.
nm -j and dyld_info -exports end each row with LF. Splitting on \r?\n and
trimming rows treated a carriage return at the end of a symbol name as part
of the line terminator, so the distinct exports "trailing\r" and
"trailing" merged into one entry with the wrong address. Split rows on LF
only and keep trailing text in export rows.
Fixes#3390
* fix(apple): read LC_LAZY_LOAD_DYLIB_INFO dependency payloads
Parse the linkedit install name for lazy-load info commands, resolve those
edges without traversing them, and stop inspect-macho from treating an otool
dependency list as exhaustive when that payload is omitted.
Fixes#5551Fixes#5649
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(apple): bound lazy-load payload reads and image offsets
Reuse identical LC_LAZY_LOAD_DYLIB_INFO ranges, cap their combined size,
and reject flag or chain-start offsets outside the slice VM range.
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(apple): bound lazy-load string scans and contained reads
Reject payload strings that start in the 24-byte header, validate each
file-offset string once, and reuse payload bytes already read for a
containing range.
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(apple): reject cached lazy-load strings outside a payload
Remember each cached string's terminating offset and require that NUL to
fall inside the payload being decoded.
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(apple): bound lazy-load payload range lookups
Count containment checks across earlier payload reads and stop at the
existing 16 MiB work limit.
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(apple): count every lazy-info string reference
Charge repeated and overlapping LC_LAZY_LOAD_DYLIB_INFO scans against the
16 MiB validation budget, and stop a scan once that budget is exhausted.
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(apple): cap retained lazy-info dependency results
Stop after 1024 LC_LAZY_LOAD_DYLIB_INFO commands so a load-command region
full of identical commands cannot become a million traced edges.
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(apple): reject a lazy-load path NUL in the final byte
dyld stops the load-path scan before the payload's last byte, so a
terminator in that byte leaves the image malformed.
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(apple): keep may-be-missing lazy loads optional
A missing LC_LAZY_LOAD_DYLIB_INFO with the may-be-missing bit is weak, so the lazy finding says dyld continues on first use instead of failing.
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(apple): cap lazy-info symbol string references
A payload of distinct one-byte symbol strings stayed under the byte budgets while caching one entry per offset. A slice now stops at 65536 symbol references.
Co-authored-by: morluto <morluto@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
Admit only the corroborated missing-UUID validation diagnostic from header-stripped dyld stdout. Preserve readable load commands and nm exports with unknown dyld coverage, and keep unrelated process and tool failures typed.
Closes#5606
Carry the selected slice name and the existing tool owner's dwarfdump spelling separately. Compare observed row labels exactly, preserving explicit ABI versions and uncertainty for missing rows.
Correct the production-boundary fixture and retain wrong-version and mixed-row controls. Real Apple multi-version selection behavior remains unverified.
Refs: #4195, #4288
Co-authored-by: Henglie <ebhenglie@gmail.com>
inspect_macho passed the CPU-family spelling (`arm64e`) for every dotted lipo
slice name to otool, nm, dwarfdump and vtool. That is right for the
pointer-auth ABI version suffix (`arm64e.v1`), but `arm64e.x1` is a distinct
subtype: on a universal x86_64 + arm64e.x1 executable otool failed with
"does not contain architecture: arm64e" and the whole inspection failed.
Strip only the `.v<N>` ABI version suffix and pass other variant names
exactly. vtool has no spelling for a variant subtype, so skip it for such
slices and report that build metadata comes from otool only.
Fixes#3389
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(native): skip llvm-nm's dyld-info pass when the symbol table names every export
inspect_macho ran `nm -gjU`, whose default dyld-info pass takes seconds on
large apps (Keynote arm64 5.8 s, x86_64 10.2 s) yet under -gU can only add
export-trie names that dyld_info -exports already reports. Run
`nm -gjU --no-dyldinfo` and rerun the default pass only when dyld_info
reports an export the symbol table lacks (e.g. strip -T), so its exact
names still disambiguate dyld_info rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(native): fall back to nm's dyld-info pass for unreadable export rows
Account for every dyld_info export line instead of only the rows the
parser returns, so an unfamiliar row format reruns the default nm pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(native): derive Mach-O word size and byte order from the selected slice
inspect_macho read endian and word_size from `file -b` for the whole
file. macOS `file` never prints a Mach-O byte order, so endian was null
for every target. For a universal binary, word_size matched "64-bit"
anywhere in the summary instead of describing the selected slice, and it
was null for a thin 32-bit slice.
Both now come from the selected slice's otool header: the magic gives
the word size and the CPU family gives the byte order (x86 and ARM
little, PowerPC big, other families unknown). The file capture is no
longer needed by inspect_macho, so it is no longer run there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(native): select a 32-bit slice beside a 64-bit slice through inspect_macho
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(native): parse renamed dyld_info re-exports
dyld_info prints a re-export whose name differs from the source library's
symbol as `[re-export] _foo_renamed (_foo from libfoo)`. The export parser
only accepted `(from libfoo)`, so inspect_macho reported a nonexistent
export named `_foo_renamed (_foo from libfoo)` with no source, and the nm
row for `_foo_renamed` survived the name-based merge as a second export
with reexport null.
Accept the optional source symbol, trying the plain `(from lib)` form
first so a library name containing ` from ` keeps its text. The row now
reports `_foo_renamed` re-exported from libfoo and replaces the nm
duplicate. The export schema has no field for the source symbol name, so
exports coverage records a limitation when a renamed re-export is present.
The golden fixture is a dyld_info capture of a synthetic dylib linked with
-Wl,-alias,_foo,_foo_renamed.
* fix(native): split dyld re-export suffix without nested quantifiers
Replace the (?:(.+?)\s+)??from lazy-optional group with a linear string
scan so malformed lines cannot backtrack and a library path containing
" from " still keeps its whole text via the plain-form-first check.
* test(native): cover nested re-export library names
* fix(native): retain reexports missing from nm inventory
---------
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: morluto <17176810+morluto@users.noreply.github.com>
* fix(native): keep lipo path echoes out of FAT records
Parse FAT records only after separating the exact echoed operand path. Pass the same lipo operand at all three consumers and cover multiline paths through parser, slice-reader and compiled CLI regressions.
* fix(native): use parsed lipo framing for container offsets
* test(native): use explicit FAT fixtures for CLI regressions
* fix(native): select one slice when universal Mach-O slices share a CPU type
On macOS 27 most system binaries are universal files whose slices share
CPU_TYPE_ARM64: arm64e + arm64e.x1 (Calculator, TextEdit), x86_64 + arm64e +
arm64e.x1 (/bin/ls), or arm64 + arm64e.x1 (ShortcutDroplet). Target admission
names slices by CPU family, so these targets have availableArchitectures
["arm64"] while the file holds two arm64 slices.
inspect_macho read that family count as a slice count and ran otool, nm,
dyld_info, dwarfdump and vtool without -arch. ShortcutDroplet failed with
"Malformed otool load-command output" (40 load commands against ncmds 20).
Calculator reported both slices' imports (4138 instead of 2069), nm's
"(for architecture ...)" header lines as exports, and no selected slice.
selectArchitecture now also filters when lipo lists more than one slice. Its
preference is unchanged: the family name, then arm64e, then the first
arm64e.<variant>.
inspect_native_dispatch_metadata matched FAT slices by CPU type only and
refused /bin/ls and TextEdit as "Ambiguous FAT architecture slice".
selectMachoSlice now ranks slices of the requested CPU type by base subtype in
the same order: arm64 (0, then 1) or x86_64 (3), then arm64e (2), then other
variants such as arm64e.x1 in FAT table order. It reports ambiguity only when
the top-ranked slices share a subtype.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(native): share Mach-O slice preference across inspections
* chore: preserve upstream process capture test during merge
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
inspect_macho launched otool, nm, two dyld_info reads, dwarfdump and
vtool one at a time. Each launch also resolves the tool through
`xcrun --find`, so a warm call spent about 350 ms in eight serial
supervised launches. The slice commands only read the selected
artifact, so launch them together. Every launch still settles before
the result, and declared order still fixes provenance and which
failure is reported.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Reconcile Mach-O UUID observations without hiding malformed or conflicting sources. Require source-map identity digests to be supported by consistent observations. Keep truncated Android manifests as partial raw evidence, validate WABT section framing, and reject missing JEB inventories or error-marked success replies.
Consolidate native UUID parsing and simplify capacity and malformed-input fixtures. Focused source and public workflow checks pass, including native arm64e inspection and real WABT framing probes; a successful real JEB GUI project opening remains unverified.
Expose the selected Mach-O slice and its container offset, derive that identity once, and map entrypoint and nonempty segment Evidence to container coordinates. Reject relocation sections whose byte length is not divisible by the entry size instead of silently accepting a floored count.
Validation: native parser and selected-architecture boundaries, actual public CLI and stdio MCP on a compiled universal Mach-O, and a pyelftools 0.33 malformed-table probe. Full Linux pwntools bridge verification was unavailable on this host.
Refs #1973
Validate Mach command counts, byte totals, scalar uniqueness, section counts, and build-tool counts before claiming exhaustive evidence. Preserve raw output in typed parsing failures and distinguish dylib commands from the dynamic loader. Correct inconsistent handcrafted fixtures and label their provenance.
Validation: 38 native tests, combined checks, and temporary-directory hygiene. Multiline dylib-name ambiguity was reproduced with the host Apple otool; regression workflows use captured output and injected command runners.
* fix: order remaining outputs independently of host locale
String.prototype.localeCompare without a pinned collation follows the
host's ICU default locale, so the same input could be ordered differently
on different machines. 614fb768 moved canonical domain projections to
compareUnicodeCodePoints; provider, application, catalog, and setup
orderings still used locale collation.
Order every remaining source site by Unicode code point, including Mach-O
symbols, firmware extraction paths, ASAR entries (previously pinned to
"en" collation), evidence ID sets, browser and Electron target, script,
and resource inventories, reconstruction obligations, capability and
provider listings, and setup environment details.
The current tool and prompt catalog names order identically under both
comparators, so the catalog identity digest is unchanged.
* fix(artifacts): order ASAR entries by their portable path
@electron/asar lists host-spelled paths, so code-point order over the raw
spelling still differed by host: "a\b" sorts after "a0" on Windows while
"a/b" sorts before it on POSIX. Pair each listing with its portable
archive path once, order by that path, and keep the host spelling for
@electron/asar calls and adapter keys.
* fix: bind identity and coverage to the producing rule
Logical paths, container families, examined bytes, path-segment conventions, destination collisions, file-id leases, Mach-O CPU selection, and firmware cleanup now follow the observation that owns each rule.
* fix: fail unmatched Mach-O slices and keep firmware roots independent
A fat target whose architecture lipo did not list now stops before slice tools run. Chained fixups still count unread LC_DYLD_INFO rebase bytes, and a firmware cleanup failure no longer blocks the next workspace.
* test: cover path identity and runtime families at the projection boundary
NFC joins and segment conventions are now asserted by the path resolver, the Apple projection, and a real APK inventory instead of helper unit tests.
* test: expect unread dyld rebase bytes in the Apple dispatch fixture
The legacy LC_DYLD_INFO fixture keeps a rebase range this reader does not decode, so the public CLI and MCP check now requires that partial coverage beside the bind opcodes.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
inspect_macho, inspect_signature, inspect_plist, list_architectures and
demangle_swift already return their command provenance inside the
normalized result. The provider also copied the same array into
raw_result, so every response carried it twice. raw_result now stays
null, as docs/mcp-contracts.md describes for results without a distinct
provider representation.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
otool and dyld_info print the operand path before a slice's records,
and the Mach-O parsers read that header as output. A file name holding
`Load command` blocks or `(from lib)` lines therefore added a forged
LC_MAIN entrypoint, corrupted the Mach header fields, and injected
imports and exports into `inspect_macho`.
Strip the exact echoed operand header (`<path>:`, `<path> (architecture
<arch>):`, `<path> [<arch>]:`) before parsing. Output without that
header, such as replayed captures, is parsed unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move each error family out of domain/errors.ts into its own module
(base, core, provider selection/adapter, browser observation,
artifact, evidence, unknown registry, hopper, configuration) and
relocate AnalysisErrorProjection to analysisErrorProjection.
Migrate all 162 importers to the owning modules and delete
errors.ts. No re-export shims; behavior is unchanged.