fix(apple): preserve line terminators in dyld path expansion (#5363)

Signed-off-by: 李永祺 <doribelove@gmail.com>
This commit is contained in:
Dori
2026-10-23 05:24:02 +08:00
committed by GitHub
parent 6b8f4f7528
commit bd2a84ccba
3 changed files with 115 additions and 1 deletions
+2 -1
View File
@@ -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")
+51
View File
@@ -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";
@@ -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();