diff --git a/src/native/NativeMachoInspection.ts b/src/native/NativeMachoInspection.ts index 5df50ef3c5..177a11b9d6 100644 --- a/src/native/NativeMachoInspection.ts +++ b/src/native/NativeMachoInspection.ts @@ -114,6 +114,15 @@ const normalizeMacho = ( ] : []), ...additionalLimitations, + ...(load.entrypoints.length === 0 && + load.commands.some( + (command) => + command.kind === "LC_UNIXTHREAD" || command.kind === "LC_THREAD", + ) + ? [ + "Thread-state entrypoints (LC_UNIXTHREAD/LC_THREAD) are retained as raw load commands; file offsets are not derived.", + ] + : []), ...(imageBase === null && /^\s*offset\s+symbol\s*$/mu.test(byTool("dyld_info", 1).stdout) ? [ diff --git a/src/native/parsers/otool.ts b/src/native/parsers/otool.ts index 619c20a005..ebb8580b82 100644 --- a/src/native/parsers/otool.ts +++ b/src/native/parsers/otool.ts @@ -125,13 +125,22 @@ const collectBuild = ( fields: Readonly>, state: ParsedLoadCommands, ): void => { - if (kind !== "LC_BUILD_VERSION") return; - state.builds.push({ - platform: stringField(fields, "platform"), - minimum_os: stringField(fields, "minos"), - sdk: stringField(fields, "sdk"), - tools: parseBuildTools(block), - }); + if (kind === "LC_BUILD_VERSION") { + state.builds.push({ + platform: stringField(fields, "platform"), + minimum_os: stringField(fields, "minos"), + sdk: stringField(fields, "sdk"), + tools: parseBuildTools(block), + }); + return; + } + if (kind.startsWith("LC_VERSION_MIN_")) + state.builds.push({ + platform: kind.slice("LC_VERSION_MIN_".length) || null, + minimum_os: stringField(fields, "version"), + sdk: stringField(fields, "sdk"), + tools: [], + }); }; const collectDependency = ( @@ -142,7 +151,8 @@ const collectDependency = ( if ( !kind.startsWith("LC_LOAD_") && kind !== "LC_ID_DYLIB" && - kind !== "LC_REEXPORT_DYLIB" + kind !== "LC_REEXPORT_DYLIB" && + kind !== "LC_LAZY_LOAD_DYLIB" ) return; state.dependencies.push({ diff --git a/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts b/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts new file mode 100644 index 0000000000..409d993145 --- /dev/null +++ b/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts @@ -0,0 +1,85 @@ +import { expect, it } from "vitest"; + +import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; +import { parseOtoolLoadCommands } from "../../../../src/native/parsers/otool.js"; +import { + NativeFixtureRunner, + nativeMachoTarget, +} from "../../../fixtures/nativeCommands.js"; + +it("retains lazily loaded library dependencies", () => { + const load = parseOtoolLoadCommands( + [ + "Load command 9", + " cmd LC_LAZY_LOAD_DYLIB", + " name /usr/lib/libLazy.dylib (offset 24)", + " time stamp 2 Thu Jan 1 00:00:02 1970", + " current version 1.0.0", + "compatibility version 1.0.0", + "", + ].join("\n"), + ); + expect(load.dependencies).toContainEqual({ + path: "/usr/lib/libLazy.dylib", + kind: "LC_LAZY_LOAD_DYLIB", + current_version: "1.0.0", + compatibility_version: "1.0.0", + }); +}); + +it("normalizes legacy version-minimum commands into build metadata", () => { + const load = parseOtoolLoadCommands( + [ + "Load command 3", + " cmd LC_VERSION_MIN_MACOSX", + " version 10.13", + " sdk 10.15", + "Load command 4", + " cmd LC_VERSION_MIN_IPHONEOS", + " version 12.0", + " sdk 12.1", + "Load command 5", + " cmd LC_SOURCE_VERSION", + " version 1.2.3", + "", + ].join("\n"), + ); + expect(load.builds).toEqual([ + { + platform: "MACOSX", + minimum_os: "10.13", + sdk: "10.15", + tools: [], + }, + { + platform: "IPHONEOS", + minimum_os: "12.0", + sdk: "12.1", + tools: [], + }, + ]); +}); + +it("limits thread-state entrypoints instead of reporting none silently", async () => { + const runner = new NativeFixtureRunner({ + "otool:-h": [ + "Load command 0", + " cmd LC_UNIXTHREAD", + " flavor 1", + " count 1", + "", + ].join("\n"), + }); + const client = new NativeMacOSProvider(runner, "darwin").createClient( + nativeMachoTarget("/owned/legacy-fixture"), + ); + const result = await client.execute("inspect_macho", {}); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.result).toMatchObject({ + entrypoints: { items: [] }, + }); + expect(result.value.limitations).toContain( + "Thread-state entrypoints (LC_UNIXTHREAD/LC_THREAD) are retained as raw load commands; file offsets are not derived.", + ); +});