fix(native): capture Mach-O header metadata alongside load commands (#525)

* fix(native): capture Mach-O header metadata alongside load commands

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* style(test): format native Mach-O header regression

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* fix(native): drop dead bare otool override in header test

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Rudy Mizrahi Celekli 2026-10-05 02:50:58 -04:00 committed by GitHub
parent 31ed005183
commit a8195ba2cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 360 additions and 12 deletions

View File

@ -35,7 +35,7 @@ interface NativeMachoInspectionContext {
const REQUIRED_COMMANDS = [
["file", ["-b"]],
["lipo", ["-detailed_info"]],
["otool", ["-l"]],
["otool", ["-h", "-l"]],
["nm", ["-gjU"]],
["dyld_info", ["-imports"]],
["dyld_info", ["-exports"]],

View File

@ -98,7 +98,6 @@ const parseLoadCommand = (block: string, state: ParsedLoadCommands): void => {
collectBuild(kind, block, fields, state);
collectDependency(kind, fields, state);
collectSegment(kind, block, fields, state);
collectFlags(fields, state.flags);
};
const collectUuid = (
@ -159,16 +158,6 @@ const collectSegment = (
state.segments.push(parseSegment(block, fields));
};
const collectFlags = (
fields: Readonly<Record<string, string | number | null>>,
flags: Set<string>,
): void => {
const rawFlags = stringField(fields, "flags");
if (rawFlags === null) return;
for (const flag of rawFlags.split(/\s+/u))
if (flag.length > 0) flags.add(flag);
};
const parseFields = (block: string): Record<string, string | number | null> => {
const fields: Record<string, string | number | null> = {};
for (const rawLine of block.split(/\r?\n/u).slice(1)) {

View File

@ -0,0 +1,44 @@
import { expect, it } from "vitest";
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
import { parseOtoolLoadCommands } from "../../../../src/native/parsers/otool.js";
import {
NativeFixtureRunner,
nativeFixture,
nativeMachoTarget,
} from "../../../fixtures/nativeCommands.js";
it("collects the actual Mach header alongside the native load commands", async () => {
// The provider always invokes `otool -h -l`, which the runner resolves
// via the "otool:-h" key; no bare `otool` override is needed.
const runner = new NativeFixtureRunner({
"otool:-h": await nativeFixture(
"native-macho-header/otool-header-load.txt",
),
});
const client = new NativeMacOSProvider(runner, "darwin").createClient(
nativeMachoTarget("/owned/fixture"),
);
const result = await client.execute("inspect_macho", {});
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.result).toMatchObject({
file_type: "2",
flags: ["0x00200085"],
segments: {
items: expect.arrayContaining([
expect.objectContaining({ name: "__TEXT" }),
]),
},
});
});
it("does not reinterpret segment and section flags as Mach header flags", async () => {
const load = parseOtoolLoadCommands(
await nativeFixture("native-macho-header/otool-header-load.txt"),
);
expect(load.flags).toEqual(["0x00200085"]);
expect(load.commands.some((command) => command.fields.flags === 0)).toBe(
true,
);
});

View File

@ -0,0 +1,14 @@
# Source-built Mach-O header capture
Captured on macOS arm64 with the host Xcode tools from the adjacent `fixture.c`:
```sh
xcrun clang fixture.c -o fixture
xcrun otool -l fixture > otool-load.txt
{ xcrun otool -h fixture; xcrun otool -l fixture; } > otool-header-load.txt
```
Only the temporary fixture path was replaced with `/owned/fixture`; tool fields
are unchanged. The first output contains load commands without a Mach header.
The second contains the real numeric header followed by those same commands.
These are format regression captures, not Hopper or Ghidra runtime proof.

View File

@ -0,0 +1,2 @@
int owned_value(void) { return 7; }
int main(void) { return owned_value(); }

View File

@ -0,0 +1,152 @@
/owned/fixture:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777228 0 0x00 2 16 744 0x00200085
/owned/fixture:
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot 0x00000000
initprot 0x00000000
nsects 0
flags 0x0
Load command 1
cmd LC_SEGMENT_64
cmdsize 232
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000004000
fileoff 0
filesize 16384
maxprot 0x00000005
initprot 0x00000005
nsects 2
flags 0x0
Section
sectname __text
segname __TEXT
addr 0x0000000100000328
size 0x0000000000000028
offset 808
align 2^2 (4)
reloff 0
nreloc 0
flags 0x80000400
reserved1 0
reserved2 0
Section
sectname __unwind_info
segname __TEXT
addr 0x0000000100000350
size 0x0000000000000060
offset 848
align 2^2 (4)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
Load command 2
cmd LC_SEGMENT_64
cmdsize 72
segname __LINKEDIT
vmaddr 0x0000000100004000
vmsize 0x0000000000004000
fileoff 16384
filesize 504
maxprot 0x00000001
initprot 0x00000001
nsects 0
flags 0x0
Load command 3
cmd LC_DYLD_CHAINED_FIXUPS
cmdsize 16
dataoff 16384
datasize 56
Load command 4
cmd LC_DYLD_EXPORTS_TRIE
cmdsize 16
dataoff 16440
datasize 64
Load command 5
cmd LC_SYMTAB
cmdsize 24
symoff 16512
nsyms 3
stroff 16560
strsize 48
Load command 6
cmd LC_DYSYMTAB
cmdsize 80
ilocalsym 0
nlocalsym 0
iextdefsym 0
nextdefsym 3
iundefsym 3
nundefsym 0
tocoff 0
ntoc 0
modtaboff 0
nmodtab 0
extrefsymoff 0
nextrefsyms 0
indirectsymoff 0
nindirectsyms 0
extreloff 0
nextrel 0
locreloff 0
nlocrel 0
Load command 7
cmd LC_LOAD_DYLINKER
cmdsize 32
name /usr/lib/dyld (offset 12)
Load command 8
cmd LC_UUID
cmdsize 24
uuid 633B21A4-D14A-3312-A5AD-B3CB919DAC72
Load command 9
cmd LC_BUILD_VERSION
cmdsize 32
platform 1
minos 26.0
sdk 26.4
ntools 1
tool 3
version 1266.8
Load command 10
cmd LC_SOURCE_VERSION
cmdsize 16
version 0.0
Load command 11
cmd LC_MAIN
cmdsize 24
entryoff 816
stacksize 0
Load command 12
cmd LC_LOAD_DYLIB
cmdsize 56
name /usr/lib/libSystem.B.dylib (offset 24)
time stamp 2 Wed Dec 31 19:00:02 1969
current version 1356.0.0
compatibility version 1.0.0
Load command 13
cmd LC_FUNCTION_STARTS
cmdsize 16
dataoff 16504
datasize 8
Load command 14
cmd LC_DATA_IN_CODE
cmdsize 16
dataoff 16512
datasize 0
Load command 15
cmd LC_CODE_SIGNATURE
cmdsize 16
dataoff 16608
datasize 280

View File

@ -0,0 +1,147 @@
/owned/fixture:
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot 0x00000000
initprot 0x00000000
nsects 0
flags 0x0
Load command 1
cmd LC_SEGMENT_64
cmdsize 232
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000004000
fileoff 0
filesize 16384
maxprot 0x00000005
initprot 0x00000005
nsects 2
flags 0x0
Section
sectname __text
segname __TEXT
addr 0x0000000100000328
size 0x0000000000000028
offset 808
align 2^2 (4)
reloff 0
nreloc 0
flags 0x80000400
reserved1 0
reserved2 0
Section
sectname __unwind_info
segname __TEXT
addr 0x0000000100000350
size 0x0000000000000060
offset 848
align 2^2 (4)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
Load command 2
cmd LC_SEGMENT_64
cmdsize 72
segname __LINKEDIT
vmaddr 0x0000000100004000
vmsize 0x0000000000004000
fileoff 16384
filesize 504
maxprot 0x00000001
initprot 0x00000001
nsects 0
flags 0x0
Load command 3
cmd LC_DYLD_CHAINED_FIXUPS
cmdsize 16
dataoff 16384
datasize 56
Load command 4
cmd LC_DYLD_EXPORTS_TRIE
cmdsize 16
dataoff 16440
datasize 64
Load command 5
cmd LC_SYMTAB
cmdsize 24
symoff 16512
nsyms 3
stroff 16560
strsize 48
Load command 6
cmd LC_DYSYMTAB
cmdsize 80
ilocalsym 0
nlocalsym 0
iextdefsym 0
nextdefsym 3
iundefsym 3
nundefsym 0
tocoff 0
ntoc 0
modtaboff 0
nmodtab 0
extrefsymoff 0
nextrefsyms 0
indirectsymoff 0
nindirectsyms 0
extreloff 0
nextrel 0
locreloff 0
nlocrel 0
Load command 7
cmd LC_LOAD_DYLINKER
cmdsize 32
name /usr/lib/dyld (offset 12)
Load command 8
cmd LC_UUID
cmdsize 24
uuid 633B21A4-D14A-3312-A5AD-B3CB919DAC72
Load command 9
cmd LC_BUILD_VERSION
cmdsize 32
platform 1
minos 26.0
sdk 26.4
ntools 1
tool 3
version 1266.8
Load command 10
cmd LC_SOURCE_VERSION
cmdsize 16
version 0.0
Load command 11
cmd LC_MAIN
cmdsize 24
entryoff 816
stacksize 0
Load command 12
cmd LC_LOAD_DYLIB
cmdsize 56
name /usr/lib/libSystem.B.dylib (offset 24)
time stamp 2 Wed Dec 31 19:00:02 1969
current version 1356.0.0
compatibility version 1.0.0
Load command 13
cmd LC_FUNCTION_STARTS
cmdsize 16
dataoff 16504
datasize 8
Load command 14
cmd LC_DATA_IN_CODE
cmdsize 16
dataoff 16512
datasize 0
Load command 15
cmd LC_CODE_SIGNATURE
cmdsize 16
dataoff 16608
datasize 280