From 65726afaaaca04b7a46e85bdb195b2d83386dcfb Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Thu, 15 Oct 2026 03:42:15 -0700 Subject: [PATCH] fix(native): report an object file's unnamed segment by its empty name (#4051) inspect-macho projected an empty Mach-O segment or section name as the display label "unknown", so an exhaustive segment inventory for an ordinary MH_OBJECT reported a segment literally named "unknown" while the raw LC_SEGMENT_64 field showed "". Object files place every section in one unnamed segment, so the empty name is a source value. Segment and section names are now reported exactly as observed. Fixes #3875 Co-authored-by: Claude Opus 5.5 --- src/native/parsers/otool.ts | 21 +++++-- .../cli/nativeMachoObjectSegmentCli.test.ts | 55 +++++++++++++++++++ .../native/nativeLoadKinds.golden.test.ts | 10 ++-- 3 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 tests/boundary/cli/nativeMachoObjectSegmentCli.test.ts diff --git a/src/native/parsers/otool.ts b/src/native/parsers/otool.ts index 03e1a34ad6..bf135bf9d5 100644 --- a/src/native/parsers/otool.ts +++ b/src/native/parsers/otool.ts @@ -494,7 +494,7 @@ const parseSegment = ( block: string, fields: Readonly>, ): ParsedSegment => ({ - name: nonEmptyName(stringField(fields, "segname")), + name: machoName(fields, "segname"), vm_address: hexField(fields, "vmaddr"), vm_size: numberField(fields, "vmsize"), file_offset: numberField(fields, "fileoff"), @@ -522,8 +522,8 @@ const parseSections = (block: string): ParsedSection[] => ]) requireUniqueField(parsed, key); return { - segment: nonEmptyName(stringField(fields, "segname")), - name: nonEmptyName(stringField(fields, "sectname")), + segment: machoName(fields, "segname"), + name: machoName(fields, "sectname"), address: hexField(fields, "addr"), size: numberField(fields, "size"), file_offset: numberField(fields, "offset"), @@ -578,9 +578,18 @@ const numeric = (value: string): number | null => { return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null; }; -/** Project an absent or empty Mach-O name as before: the "unknown" label. */ -const nonEmptyName = (name: string | null): string => - name === null || name.length === 0 ? "unknown" : name; +/** + * A Mach-O segment or section name exactly as observed. Object files place + * every section in one unnamed segment, so the empty name is a source value. + */ +const machoName = ( + fields: Readonly>, + key: "segname" | "sectname", +): string => { + const name = stringField(fields, key); + if (name === null) throw new TypeError(`otool field ${key} is missing`); + return name; +}; const stringField = ( fields: Readonly>, diff --git a/tests/boundary/cli/nativeMachoObjectSegmentCli.test.ts b/tests/boundary/cli/nativeMachoObjectSegmentCli.test.ts new file mode 100644 index 0000000000..4d68b30bda --- /dev/null +++ b/tests/boundary/cli/nativeMachoObjectSegmentCli.test.ts @@ -0,0 +1,55 @@ +import { join } from "node:path"; +import { writeFile } from "node:fs/promises"; + +import { describe, expect } from "vitest"; +import { z } from "zod"; + +import { inspectMachoSchema } from "../../../src/domain/native/nativeInspection.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +const evidenceSchema = z.object({ normalized_result: inspectMachoSchema }); + +describe.skipIf(process.platform !== "darwin")( + "native Mach-O CLI object segment", + () => { + cliTest( + "reports an object file's unnamed segment by its observed empty name", + async ({ cli, processes }) => { + const directory = await createTestTempDirectory("rea-object-segment-"); + const source = join(directory, "qa.c"); + const object = join(directory, "qa.o"); + await writeFile(source, "int qa(void) { return 7; }\n"); + const compiled = await processes.run( + "/usr/bin/xcrun", + ["clang", "-c", source, "-o", object], + { timeoutMs: 30_000 }, + ); + expect(compiled.exitCode, compiled.stderr).toBe(0); + const result = await cli.run({ + arguments: ["inspect-macho", object, "--json"], + environment: { REA_LOG_LEVEL: "silent" }, + }); + expect(result.exitCode).toBe(0); + const inspected = evidenceSchema.parse(result.json).normalized_result; + expect(inspected.load_commands.items[0]?.fields.segname).toBe(""); + expect(inspected.segments).toMatchObject({ + items: [ + { + name: "", + sections: { + items: expect.arrayContaining([ + expect.objectContaining({ + segment: "__TEXT", + name: "__text", + }), + ]), + }, + }, + ], + exhaustive: true, + }); + }, + ); + }, +); diff --git a/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts b/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts index 3d744fbd8f..84edc6b2ac 100644 --- a/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts +++ b/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts @@ -112,7 +112,7 @@ it.each(["vmaddr", "vmsize", "uuid", "entryoff"])( }, ); -it("retains the unnamed segment of a Mach-O object file", () => { +it("retains the empty segment name of a Mach-O object file", () => { // Captured from `otool -l` on a clang `-c` object: MH_OBJECT sections live // in one segment whose segname is empty, printed as a bare label. const objectSegment = [ @@ -143,17 +143,19 @@ it("retains the unnamed segment of a Mach-O object file", () => { "", ].join("\n"); const load = parseOtoolLoadCommands(withOtoolMachHeader(objectSegment)); + // The empty name is the observed source value, as in the raw command. + expect(load.commands[0]?.fields.segname).toBe(""); expect(load.segments).toMatchObject([ - { name: "unknown", sections: [{ segment: "__TEXT", name: "__text" }] }, + { name: "", sections: [{ segment: "__TEXT", name: "__text" }] }, ]); - // An all-NUL section name is printed the same way and stays "unknown". + // An all-NUL section name is printed the same way and stays empty. expect( parseOtoolLoadCommands( withOtoolMachHeader( objectSegment.replace(" sectname __text", " sectname "), ), ).segments, - ).toMatchObject([{ sections: [{ segment: "__TEXT", name: "unknown" }] }]); + ).toMatchObject([{ sections: [{ segment: "__TEXT", name: "" }] }]); expect(() => parseOtoolLoadCommands( withOtoolMachHeader(