From c19d319929cedb74f9d50138c415919197ef3acd Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Thu, 12 Nov 2026 16:33:16 -0800 Subject: [PATCH] fix(desktop): preserve literal directories in ELF origin expansion (#10215) fix(desktop): expand ELF origin paths as literal directory names --- src/desktop/LibraryPackageInventory.test.ts | 44 +++++++++++++++++++++ src/domain/desktop/libraryPackages.test.ts | 27 +++++++++++++ src/domain/desktop/libraryPackages.ts | 10 +++-- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/desktop/LibraryPackageInventory.test.ts b/src/desktop/LibraryPackageInventory.test.ts index 5f72a72202..2c0e4ac37f 100644 --- a/src/desktop/LibraryPackageInventory.test.ts +++ b/src/desktop/LibraryPackageInventory.test.ts @@ -10,6 +10,50 @@ import { inspectLibraryPackages } from "./LibraryPackageInventory.js"; import { elfImage } from "../domain/desktop/elfImage.fixture.js"; import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; +describe.skipIf(process.platform === "win32")( + "literal ELF origin paths", + () => { + it.each(["rpath", "runpath"] as const)( + "resolves %s libraries when the executable's directory contains dollars", + async (tag) => { + const root = await createTestTempDirectory("rea-library-origin-"); + const bin = join(root, "$build-$&", "bin"); + const lib = join(root, "$build-$&", "lib"); + await Promise.all([ + mkdir(bin, { recursive: true }), + mkdir(lib, { recursive: true }), + ]); + const executable = join(bin, "app"); + const library = join(lib, "liborigin.so"); + await Promise.all([ + writeFile( + executable, + elfImage({ needed: ["liborigin.so"], [tag]: "${ORIGIN}/../lib" }), + ), + writeFile(library, elfImage({})), + ]); + const queried: string[] = []; + const result = await inspectLibraryPackages( + executable, + "app", + async (command, args) => { + if (command === "ldconfig") return { stdout: "", exitCode: 0 }; + queried.push(...args.slice(1)); + return { stdout: `origin-owner: ${library}`, exitCode: 0 }; + }, + undefined, + ); + expect(result).toEqual({ + library_packages: ["origin-owner"], + missing_libraries: [], + unpackaged_libraries: [], + }); + expect(queried).toContain(library); + }, + ); + }, +); + it.skipIf(process.platform === "win32")( "retains a compatible candidate's package when its declared header size is invalid", async () => { diff --git a/src/domain/desktop/libraryPackages.test.ts b/src/domain/desktop/libraryPackages.test.ts index 2a5e4803d9..be9bdc818d 100644 --- a/src/domain/desktop/libraryPackages.test.ts +++ b/src/domain/desktop/libraryPackages.test.ts @@ -325,6 +325,33 @@ describe("libraryClosure unresolved candidates", () => { }); describe("libraryClosure search paths", () => { + it("expands ORIGIN literally while leaving unsupported source tokens unresolved", async () => { + const closure = await libraryClosure( + { + path: "/opt/$build-$&/bin/app", + dependencies: elf( + ["liborigin.so", "libunsupported.so"], + ["$ORIGIN/../lib", "$ORIGIN/$LIB", "$ORIGIN_SUFFIX", "relative"], + ), + }, + [], + host({ + "/opt/$build-$&/lib/liborigin.so": elf([]), + "/opt/$build-$&/bin/$LIB/libunsupported.so": elf([]), + "/opt/$build-$&/bin_SUFFIX/libunsupported.so": elf([]), + }), + 10, + ); + expect(closure).toEqual({ + libraries: [ + { soname: "liborigin.so", path: "/opt/$build-$&/lib/liborigin.so" }, + ], + missing: ["libunsupported.so"], + unreadable: [], + truncated: false, + }); + }); + it("normalizes $ORIGIN/.. before telling the app's own folder from system libraries", async () => { const closure = await libraryClosure( { diff --git a/src/domain/desktop/libraryPackages.ts b/src/domain/desktop/libraryPackages.ts index c899699c22..e888c3e27b 100644 --- a/src/domain/desktop/libraryPackages.ts +++ b/src/domain/desktop/libraryPackages.ts @@ -66,16 +66,18 @@ interface LoadedObject { readonly loader: LoadedObject | undefined; } +const ORIGIN_TOKEN = /\$(?:ORIGIN(?![A-Za-z0-9_])|\{ORIGIN\})/gu; + /** Absolute directories with `$ORIGIN` expanded to the owner's folder; relative and other dynamic-token paths are left out. */ const expanded = ( owner: string, directories: readonly string[], ): readonly string[] => directories - .map((path) => - path.replace(/\$(?:ORIGIN|\{ORIGIN\})/gu, posix.dirname(owner)), - ) - .filter((path) => path.startsWith("/") && !path.includes("$")) + // Check tokens before expansion: dollars in the owner's directory are data. + .filter((path) => !path.replace(ORIGIN_TOKEN, "").includes("$")) + .map((path) => path.replace(ORIGIN_TOKEN, () => posix.dirname(owner))) + .filter((path) => path.startsWith("/")) .map((path) => posix.normalize(path)); /**