diff --git a/src/native/parsers/otool.ts b/src/native/parsers/otool.ts index d4e9f59fff..03e1a34ad6 100644 --- a/src/native/parsers/otool.ts +++ b/src/native/parsers/otool.ts @@ -407,7 +407,8 @@ const collectDependency = ( fields: Readonly>, state: ParsedLoadCommands, ): void => { - if (!DYLIB_COMMANDS.has(kind)) return; + // LC_ID_DYLIB names this image itself; it stays a raw load command only. + if (!DYLIB_COMMANDS.has(kind) || kind === "LC_ID_DYLIB") return; state.dependencies.push({ path: stripOffsetSuffix(stringField(fields, "name")), kind, diff --git a/tests/boundary/cli/nativeMachoDylibIdentityCli.test.ts b/tests/boundary/cli/nativeMachoDylibIdentityCli.test.ts new file mode 100644 index 0000000000..d5a51c01a9 --- /dev/null +++ b/tests/boundary/cli/nativeMachoDylibIdentityCli.test.ts @@ -0,0 +1,57 @@ +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 dylib identity", + () => { + cliTest( + "keeps a dylib's own install name out of its dependencies", + async ({ cli, processes }) => { + const directory = await createTestTempDirectory("rea-dylib-identity-"); + const source = join(directory, "library.c"); + const library = join(directory, "libprobe.dylib"); + await writeFile(source, "int exported_value(void) { return 42; }\n"); + const compiled = await processes.run( + "/usr/bin/xcrun", + [ + "clang", + "-dynamiclib", + source, + "-Wl,-install_name,@rpath/libprobe.dylib", + "-o", + library, + ], + { timeoutMs: 30_000 }, + ); + expect(compiled.exitCode, compiled.stderr).toBe(0); + const result = await cli.run({ + arguments: ["inspect-macho", library, "--json"], + environment: { REA_LOG_LEVEL: "silent" }, + }); + expect(result.exitCode).toBe(0); + const inspected = evidenceSchema.parse(result.json).normalized_result; + expect(inspected.dependencies).toMatchObject({ + items: [ + { path: "/usr/lib/libSystem.B.dylib", kind: "LC_LOAD_DYLIB" }, + ], + total: 1, + exhaustive: true, + }); + expect( + inspected.load_commands.items.find( + ({ kind }) => kind === "LC_ID_DYLIB", + )?.fields.name, + ).toBe("@rpath/libprobe.dylib (offset 24)"); + }, + ); + }, +); diff --git a/tests/boundary/providers/native/nativeInventory.golden.test.ts b/tests/boundary/providers/native/nativeInventory.golden.test.ts index 625de4fde8..fcb5bc1040 100644 --- a/tests/boundary/providers/native/nativeInventory.golden.test.ts +++ b/tests/boundary/providers/native/nativeInventory.golden.test.ts @@ -464,14 +464,8 @@ Load command 1 { index: 1, kind: "LC_UUID" }, ], ); - expect(parsed.dependencies).toEqual([ - { - path: installName, - kind: "LC_ID_DYLIB", - current_version: "3.2.1", - compatibility_version: "1.0.0", - }, - ]); + expect(parsed.commands[0]?.fields.name).toBe(`${installName} (offset 24)`); + expect(parsed.dependencies).toEqual([]); expect(parsed.uuid).toBe("01234567-89AB-CDEF-0123-456789ABCDEF"); }, ); diff --git a/tests/boundary/providers/native/nativeLoadText.golden.test.ts b/tests/boundary/providers/native/nativeLoadText.golden.test.ts index 62e5ebad33..a67502d0ab 100644 --- a/tests/boundary/providers/native/nativeLoadText.golden.test.ts +++ b/tests/boundary/providers/native/nativeLoadText.golden.test.ts @@ -16,12 +16,10 @@ it.each([ await nativeFixture(`load-command-text-values/${slug}.txt`), ), ); - expect(load.dependencies).toContainEqual({ - path: name, - kind: "LC_ID_DYLIB", - current_version: "0.0.0", - compatibility_version: "0.0.0", - }); + // The image's own install name is identity, not a dependency. + expect(load.dependencies.map(({ kind }) => kind)).not.toContain( + "LC_ID_DYLIB", + ); expect( load.commands.find((command) => command.kind === "LC_ID_DYLIB")?.fields .name, diff --git a/tests/boundary/providers/native/nativeReexport.golden.test.ts b/tests/boundary/providers/native/nativeReexport.golden.test.ts index 024715b593..d8145ceea5 100644 --- a/tests/boundary/providers/native/nativeReexport.golden.test.ts +++ b/tests/boundary/providers/native/nativeReexport.golden.test.ts @@ -22,20 +22,21 @@ it("retains native re-export dependencies alongside ordinary and identity comman expect(result.value.result).toMatchObject({ dependencies: { exhaustive: true, - total: 3, - items: expect.arrayContaining([ + total: 2, + items: [ expect.objectContaining({ kind: "LC_REEXPORT_DYLIB", path: "@rpath/libLeaf.dylib", }), - expect.objectContaining({ - kind: "LC_ID_DYLIB", - path: "/owned/libOuter.dylib", - }), expect.objectContaining({ kind: "LC_LOAD_DYLIB", path: "/usr/lib/libSystem.B.dylib", }), + ], + }, + load_commands: { + items: expect.arrayContaining([ + expect.objectContaining({ kind: "LC_ID_DYLIB" }), ]), }, });