diff --git a/src/domain/apple/dyldPaths.ts b/src/domain/apple/dyldPaths.ts index 7feb8da936..f834077479 100644 --- a/src/domain/apple/dyldPaths.ts +++ b/src/domain/apple/dyldPaths.ts @@ -87,7 +87,8 @@ export const expandPrefix = ( readonly rootRelativePath: (absolutePath: string) => string | undefined; }, ): Expansion => { - const variable = /^@(executable_path|loader_path)(?:\/(.*))?$/u.exec(raw); + // Load-command paths can contain line terminators; the suffix is not a text line. + const variable = /^@(executable_path|loader_path)(?:\/(.*))?$/su.exec(raw); if (variable !== null) { const rest = variable[2] ?? ""; if (variable[1] === "loader_path") diff --git a/src/domain/apple/dylibResolution.test.ts b/src/domain/apple/dylibResolution.test.ts index c143738225..aab085b6ad 100644 --- a/src/domain/apple/dylibResolution.test.ts +++ b/src/domain/apple/dylibResolution.test.ts @@ -109,6 +109,57 @@ const edgeFor = ( return edge; }; +it.each(["\n", "\r", "\r\n", "\u2028", "\u2029"])( + "preserves line terminator %j in dependency names and rpaths", + async (separator) => { + const direct = `lib${separator}direct.dylib`; + const fromExecutable = `libexecutable.dylib${separator}`; + const directory = `Libraries${separator}Nested`; + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + dependencies: [ + dependency(`@loader_path/${direct}`), + dependency(`@executable_path/${fromExecutable}`), + dependency("@rpath/libsearch.dylib"), + ], + rpaths: [`@loader_path/${directory}`], + }), + [`Contents/MacOS/${direct}`]: parsed(slice()), + [`Contents/MacOS/${fromExecutable}`]: parsed(slice()), + [`Contents/MacOS/${directory}/libsearch.dylib`]: parsed(slice()), + }), + { roots: [MAIN] }, + ); + expect(trace.edges.map(({ resolution }) => resolution)).toEqual([ + { status: "resolved", image: `Contents/MacOS/${direct}` }, + { status: "resolved", image: `Contents/MacOS/${fromExecutable}` }, + { + status: "resolved", + image: `Contents/MacOS/${directory}/libsearch.dylib`, + }, + ]); + }, +); + +it.each(["@loader_path_suffix", "@executable_path_suffix", "@loader_path\n"])( + "does not expand a lookalike path token %j", + async (token) => { + const name = `${token}/lib.dylib`; + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ dependencies: [dependency(name)] }), + "Contents/MacOS/lib.dylib": parsed(slice()), + }), + { roots: [MAIN] }, + ); + expect(edgeFor(trace, MAIN, name).resolution).toEqual({ + status: "undetermined", + image: null, + }); + }, +); + describe("dyld path expansion", () => { it("searches the loading image's rpaths before its loaders'", async () => { const core = "Contents/Frameworks/Core.framework/Versions/A/Core"; diff --git a/tests/boundary/cli/dylibResolutionCli.test.ts b/tests/boundary/cli/dylibResolutionCli.test.ts index 80ca7260e3..e791047111 100644 --- a/tests/boundary/cli/dylibResolutionCli.test.ts +++ b/tests/boundary/cli/dylibResolutionCli.test.ts @@ -161,6 +161,68 @@ cliTest( }, ); +cliTest( + "resolves dependency names and rpaths containing Unicode line separators", + async ({ cli }) => { + const app = await fixtureApp(); + const library = "lib\u2028core.dylib"; + const directory = "Libraries\u2029Nested"; + await writeFiles(app, { + "Contents/MacOS/App": machoImage({ + commands: [ + buildVersionCommand(1), + dylibCommand(LC.LOAD_DYLIB, `@loader_path/${library}`), + rpathCommand(`@executable_path/${directory}`), + dylibCommand(LC.LOAD_DYLIB, "@rpath/libsearch.dylib"), + ], + }), + [`Contents/MacOS/${library}`]: machoImage({ + fileType: FILE_TYPE.dylib, + commands: [buildVersionCommand(1)], + }), + [`Contents/MacOS/${directory}/libsearch.dylib`]: machoImage({ + fileType: FILE_TYPE.dylib, + commands: [buildVersionCommand(1)], + }), + }); + const output = await cli.run({ + arguments: [ + "trace-dylib-resolution", + app, + "--root", + "Contents/MacOS/App", + "--json", + ], + environment: ENVIRONMENT, + }); + expect(output.exitCode, output.stderr).toBe(0); + const trace = dylibResolutionResultSchema.parse( + parseEvidence(output.json).normalized_result, + ); + expect( + trace.edges.map(({ install_name, resolution }) => ({ + install_name, + resolution, + })), + ).toEqual([ + { + install_name: `@loader_path/${library}`, + resolution: { + status: "resolved", + image: `Contents/MacOS/${library}`, + }, + }, + { + install_name: "@rpath/libsearch.dylib", + resolution: { + status: "resolved", + image: `Contents/MacOS/${directory}/libsearch.dylib`, + }, + }, + ]); + }, +); + describe("trace-dylib-resolution CLI", () => { cliTest("traces every executable root of an app bundle", async ({ cli }) => { const app = await fixtureApp();