fix(desktop): admit ELF library identities before selection (#9981)

* fix(desktop): admit ELF library identities before selection

Preserve known library identity through later metadata failures, skip proven
foreign candidates, and keep unknown candidate selection unresolved.

Restore the reviewed repair from closed PR #9875 against pinned main,
including the original producer and inventory regression cases.

Co-authored-by: 帅鑫 Huang <1836656514@qq.com>

* fix(desktop): search compatible linker cache candidates

---------

Co-authored-by: 帅鑫 Huang <1836656514@qq.com>
This commit is contained in:
morlutoand帅鑫 Huang authored and GitHub committed 2026-11-11 17:22:23 +08:00
1 parent 4d0f6d9b42
commit dd928466cb
9 files changed
+622 -55

No files matched your search

+10 -2
View File
@@ -272,8 +272,16 @@ looks each soname up as ld.so does: in the RPATH of the needing image and of
each image up its chain of loaders to the executable when the needing image
has no RUNPATH, then in its RUNPATH (with `$ORIGIN` expanded and paths
normalized), and then in the `ldconfig -p` cache entries for the executable's
machine. It asks `dpkg-query -S` for each library's package. Libraries in the
executable's own folder belong to the application: they are walked but not
machine. Each candidate's ELF machine, class and byte order must match the
needing image before it is selected or its dependencies are walked. A foreign
search-path candidate is skipped in favor of later directories or the cache; a
cache architecture flag alone does not establish compatibility. If a candidate's
identity cannot be read, that lookup stays unresolved, without selecting its
package or guessing that a later candidate would load. A compatible identity
remains available if a later header or dependency-table check fails; its package
is retained with an incompleteness reason. REA asks `dpkg-query -S` for each
selected library's package. Libraries in the executable's own folder belong to
the application: they are walked but not
listed, and neither is the application's own package. One batched
`apt-cache policy` then keeps only the packages the Ubuntu 24.04 archive or a
recorded vendor repository with a verified key offers, in any component its
@@ -356,6 +356,27 @@ const inspected = async (options: FixtureOptions = {}) => {
};
};
it.skipIf(process.platform === "win32")(
"carries compatible library ownership through application inventory into the export install plan",
async () => {
const { root, inspect } = await vendorFixture();
// A foreign same-name library in $ORIGIN must not shadow the native cache entry.
await writeFile(
join(root, "bin/libnss3.so"),
elfImage({ machine: 62, needed: ["libdecoy.so"] }),
);
const result = await inspect();
if (!result.ok) throw new Error(result.error.reason);
const installed = result.value.inventory.package;
expect(installed.library_packages).toEqual(["libnspr4", "libnss3"]);
expect(installed.missing_libraries).not.toContain("libdecoy.so");
expect(installed.library_packages_reason).toBeUndefined();
const plan = exportInstall(installed, "typora", "arm64");
if (!plan.ok) throw new Error(plan.error.reason);
expect(plan.value.libraryPackages).toEqual(["libnspr4", "libnss3"]);
},
);
describe.skipIf(process.platform === "win32")("vendor repositories", () => {
it("records the repository apt names with the key gpgv verified, though another signature fails gpgv", async () => {
const {
@@ -10,6 +10,99 @@ import { inspectLibraryPackages } from "./LibraryPackageInventory.js";
import { elfImage } from "../domain/desktop/elfImage.fixture.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
it.skipIf(process.platform === "win32")(
"retains a compatible candidate's package when its declared header size is invalid",
async () => {
const root = await createTestTempDirectory("rea-library-header-");
const bin = join(root, "bin"),
libraries = join(root, "lib");
await Promise.all([mkdir(bin), mkdir(libraries)]);
const executable = join(bin, "app"),
library = join(libraries, "libchoice.so");
const candidate = elfImage({ needed: ["libunknown.so"] });
candidate.writeUInt16LE(0, 52);
await Promise.all([
writeFile(
executable,
elfImage({ needed: ["libchoice.so"], runpath: libraries }),
),
writeFile(library, candidate),
]);
const result = await inspectLibraryPackages(
executable,
"app",
async (command, args) =>
command === "ldconfig"
? { stdout: "", exitCode: 0 }
: {
stdout: args
.slice(1)
.map((path) => `native-owner: ${path}`)
.join("\n"),
exitCode: 0,
},
undefined,
);
expect(result).toEqual({
library_packages: ["native-owner"],
missing_libraries: [],
unpackaged_libraries: [],
library_packages_reason: expect.stringContaining(
"invalid ELF header size",
),
});
},
);
it.skipIf(process.platform === "win32")(
"queries ownership only for the compatible library selected after a foreign RPATH candidate",
async () => {
const root = await createTestTempDirectory("rea-library-architecture-");
const bin = join(root, "bin"),
foreign = join(root, "foreign"),
native = join(root, "native");
await Promise.all([mkdir(bin), mkdir(foreign), mkdir(native)]);
const executable = join(bin, "app");
const rejected = join(foreign, "libchoice.so"),
selected = join(native, "libchoice.so");
await Promise.all([
writeFile(
executable,
elfImage({ needed: ["libchoice.so"], runpath: `${foreign}:${native}` }),
),
writeFile(rejected, elfImage({ machine: 62, needed: ["libdecoy.so"] })),
writeFile(selected, 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: args
.slice(1)
.map(
(path) =>
`${path === selected ? "native-owner" : "foreign-owner"}: ${path}`,
)
.join("\n"),
exitCode: 0,
};
},
undefined,
);
expect(result).toEqual({
library_packages: ["native-owner"],
missing_libraries: [],
unpackaged_libraries: [],
});
expect(queried).toContain(selected);
expect(queried).not.toContain(rejected);
},
);
describe.skipIf(
process.platform !== "linux" || !existsSync("/var/lib/dpkg/status"),
)("inspectLibraryPackages on a Debian host", () => {
+15 -4
View File
@@ -9,6 +9,8 @@ import type { DesktopPackage } from "../domain/desktop/desktopCapture.js";
import {
readElfDependencies,
type ElfDependencies,
type ElfDependenciesFailure,
type ElfLibraryIdentity,
} from "../domain/desktop/elfDependencies.js";
import {
installableLibraryPackages,
@@ -46,7 +48,8 @@ const elfDependencies = async (
path: string,
signal: AbortSignal | undefined,
policy: DesktopLibraryPolicy,
): Promise<Result<ElfDependencies, string>> => {
expected?: ElfLibraryIdentity,
): Promise<Result<ElfDependencies, ElfDependenciesFailure>> => {
const options = {
symlinks: "follow" as const,
...(signal === undefined ? {} : { signal }),
@@ -57,6 +60,7 @@ const elfDependencies = async (
(offset, length) => readFileHandleRange(handle, offset, length),
initial.size,
policy,
expected,
),
);
} catch (cause: unknown) {
@@ -64,7 +68,11 @@ const elfDependencies = async (
throw hostCloseFailure(cause);
signal?.throwIfAborted();
if (!isRefusedRead(cause)) throw cause;
return err("the file could not be read");
return err({
kind: "unreadable",
identity: null,
reason: "the file could not be read",
});
}
};
@@ -157,7 +165,9 @@ export const inspectLibraryPackages = async (
});
const root = await elfDependencies(executable, signal, policy);
if (!root.ok)
return none(`the libraries of ${executable} were not read: ${root.error}`);
return none(
`the libraries of ${executable} were not read: ${root.error.reason}`,
);
const cache = await linkerCache(runQuery, signal);
if (cache === undefined)
return none("ldconfig -p did not list the dynamic linker cache");
@@ -165,7 +175,8 @@ export const inspectLibraryPackages = async (
{ path: executable, dependencies: root.value },
parseLinkerCache(cache),
{
dependencies: (path) => elfDependencies(path, signal, policy),
dependencies: (path, expected) =>
elfDependencies(path, signal, policy, expected),
isFile: isHostFile,
},
policy.libraries,
+105 -4
View File
@@ -24,6 +24,7 @@ describe("readElfDependencies", () => {
value: {
machine: 183,
bits: 64,
byte_order: "little",
needed: [
"libffmpeg.so",
"libnss3.so",
@@ -65,7 +66,9 @@ describe("readElfDependencies", () => {
const image = elfImage({ needed: names });
expect(await dependencies(image)).toMatchObject({
ok: false,
error: expect.stringContaining("library-policy.libraries=512"),
error: {
reason: expect.stringContaining("library-policy.libraries=512"),
},
});
expect(
await readElfDependencies(
@@ -81,7 +84,11 @@ describe("readElfDependencies", () => {
const image = elfImage({ needed: [needed] });
expect(await dependencies(image)).toMatchObject({
ok: false,
error: expect.stringContaining("library-policy.string_bytes=4096 bytes"),
error: {
reason: expect.stringContaining(
"library-policy.string_bytes=4096 bytes",
),
},
});
expect(
await readElfDependencies(
@@ -98,12 +105,102 @@ describe("readElfDependencies", () => {
value: { needed: [], runpath: undefined, rpath: [] },
});
});
});
describe("ELF dependency identity and failures", () => {
it.each(["size", "version"])(
"retains decoded identity when fixed-header %s admission fails",
async (field) => {
const image = elfImage({ needed: ["libcore.so"] });
if (field === "size") image.writeUInt16LE(0, 52);
else image.writeUInt32LE(2, 20);
expect(await dependencies(image)).toMatchObject({
ok: false,
error: {
kind: "unreadable",
identity: { machine: 183, bits: 64, byte_order: "little" },
reason: expect.stringContaining(
field === "size" ? "header size" : "header version",
),
},
});
},
);
it("classifies a foreign candidate before its declared header extent is admitted", async () => {
const image = elfImage({});
image.writeUInt16LE(0, 52);
expect(
await readElfDependencies(
readImage(image),
image.length,
desktopLibraryPolicy(),
{ machine: 62, bits: 64, byte_order: "little" },
),
).toMatchObject({
ok: false,
error: {
kind: "incompatible",
identity: { machine: 183, bits: 64, byte_order: "little" },
},
});
});
it.each([
{ machine: 62, bits: 64 as const, byte_order: "little" as const },
{ machine: 183, bits: 32 as const, byte_order: "little" as const },
{ machine: 183, bits: 64 as const, byte_order: "big" as const },
])(
"rejects incompatible identity before reading dependency tables ($machine / $bits / $byte_order)",
async (expected) => {
const image = elfImage({ needed: ["libdecoy.so"], unterminated: true });
const reads: [number, number][] = [];
const result = await readElfDependencies(
async (offset, length) => {
reads.push([offset, length]);
return readImage(image)(offset, length);
},
image.length,
desktopLibraryPolicy({ table_bytes: 1 }),
expected,
);
expect(result).toMatchObject({
ok: false,
error: {
kind: "incompatible",
identity: { machine: 183, bits: 64, byte_order: "little" },
},
});
expect(reads).toEqual([[0, 64]]);
},
);
it("retains fixed-header identity when dependency parsing or its table allowance fails", async () => {
const image = elfImage({ needed: ["libcore.so"], unterminated: true });
for (const policy of [
desktopLibraryPolicy(),
desktopLibraryPolicy({ table_bytes: 1 }),
]) {
const result = await readElfDependencies(
readImage(image),
image.length,
policy,
);
expect(result).toMatchObject({
ok: false,
error: {
identity: { machine: 183, bits: 64, byte_order: "little" },
reason: expect.any(String),
},
});
}
});
it("refuses a script, a truncated image and a dynamic table without DT_NULL", async () => {
const script = Buffer.from('#!/bin/sh\nexec /opt/app/app "$@"\n');
expect(await dependencies(script)).toEqual({
ok: false,
error: "not an ELF image",
error: { kind: "unreadable", identity: null, reason: "not an ELF image" },
});
const image = elfImage({ needed: ["libc.so.6"] });
expect(
@@ -115,7 +212,11 @@ describe("readElfDependencies", () => {
),
).toEqual({
ok: false,
error: "ELF dynamic table has no DT_NULL terminator",
error: {
kind: "unreadable",
identity: { machine: 183, bits: 64, byte_order: "little" },
reason: "ELF dynamic table has no DT_NULL terminator",
},
});
});
});
+77 -11
View File
@@ -4,7 +4,10 @@ import {
type DesktopLibraryPolicy,
} from "./libraryPolicy.js";
import { readElfDynamicEntry } from "../elfDynamicEntry.js";
import { readElfFixedHeader } from "../elfFixedHeader.js";
import {
admitElfFixedHeaderFacts,
readElfHeaderFacts,
} from "../elfFixedHeader.js";
import {
readElfProgramHeader,
type ElfProgramHeader,
@@ -18,10 +21,37 @@ import { err, ok, type Result } from "../result.js";
/** Read up to `length` bytes at `offset`; fewer at the end of the file. */
export type ReadAt = (offset: number, length: number) => Promise<Uint8Array>;
/** What the dynamic linker needs to load an ELF image's dependencies. */
export interface ElfDependencies {
/** Fixed-header identity needed before selecting a library candidate. */
export interface ElfLibraryIdentity {
readonly machine: number;
readonly bits: 32 | 64;
readonly byte_order: "little" | "big";
}
/** A readable identity survives later header or dependency-table admission failures. */
export type ElfDependenciesFailure =
| {
readonly kind: "incompatible";
readonly identity: ElfLibraryIdentity;
readonly reason: string;
}
| {
readonly kind: "unreadable";
readonly identity: ElfLibraryIdentity | null;
readonly reason: string;
};
/** The same ABI identity is required before dependency tables can select a library. */
export const elfLibraryIdentityMatches = (
expected: ElfLibraryIdentity,
observed: ElfLibraryIdentity,
): boolean =>
expected.machine === observed.machine &&
expected.bits === observed.bits &&
expected.byte_order === observed.byte_order;
/** What the dynamic linker needs to load an ELF image's dependencies. */
export interface ElfDependencies extends ElfLibraryIdentity {
/** Distinct DT_NEEDED sonames in order. */
readonly needed: readonly string[];
/** DT_RUNPATH directories as written; undefined without DT_RUNPATH. */
@@ -231,15 +261,53 @@ export const readElfDependencies = async (
read: ReadAt,
size: number,
policy: DesktopLibraryPolicy = desktopLibraryPolicy(),
): Promise<Result<ElfDependencies, string>> => {
expected?: ElfLibraryIdentity,
): Promise<Result<ElfDependencies, ElfDependenciesFailure>> => {
const header = Buffer.from(await read(0, 64));
const fixed = readElfFixedHeader(header, size);
if (!fixed.ok) return fixed;
const { elfClass: bits, machine } = fixed.value;
const little = fixed.value.byteOrder === "little";
const facts = readElfHeaderFacts(header, size);
if (!facts.ok)
return err({ kind: "unreadable", identity: null, reason: facts.error });
const identity: ElfLibraryIdentity = {
machine: facts.value.machine,
bits: facts.value.elfClass,
byte_order: facts.value.byteOrder,
};
if (expected !== undefined && !elfLibraryIdentityMatches(expected, identity))
return err({
kind: "incompatible",
identity,
reason: `Incompatible ELF library: expected machine ${expected.machine}, ELF${expected.bits}, ${expected.byte_order}-endian; observed machine ${identity.machine}, ELF${identity.bits}, ${identity.byte_order}-endian`,
});
const fixed = admitElfFixedHeaderFacts(facts.value, size);
if (!fixed.ok)
return err({ kind: "unreadable", identity, reason: fixed.error });
const dependencies = await readDependencyTables(
read,
header,
size,
identity,
policy,
);
return dependencies.ok
? ok({ ...identity, ...dependencies.value })
: err({ kind: "unreadable", identity, reason: dependencies.error });
};
/** Parse dependency tables only after their candidate identity has been retained. */
const readDependencyTables = async (
read: ReadAt,
header: Buffer,
size: number,
identity: ElfLibraryIdentity,
policy: DesktopLibraryPolicy,
): Promise<
Result<Pick<ElfDependencies, "needed" | "runpath" | "rpath">, string>
> => {
const { bits } = identity;
const little = identity.byte_order === "little";
const headers = await programHeaders(read, header, size, bits, policy);
if (!headers.ok) return headers;
const none = { machine, bits, needed: [], runpath: undefined, rpath: [] };
const none = { needed: [], runpath: undefined, rpath: [] };
const dynamic = headers.value.find((program) => program.type === PT_DYNAMIC);
if (dynamic === undefined) return ok(none);
if (dynamic.fileSize > BigInt(policy.table_bytes))
@@ -271,8 +339,6 @@ export const readElfDependencies = async (
);
if (!search.ok) return search;
return ok({
machine,
bits,
needed: [...new Set(names.value)],
runpath: runpath.size > 0 ? directories(search.value) : undefined,
rpath: runpath.size > 0 ? [] : directories(search.value),
+208 -9
View File
@@ -1,7 +1,11 @@
import { describe, expect, it } from "vitest";
import { err, ok } from "../result.js";
import type { ElfDependencies } from "./elfDependencies.js";
import {
elfLibraryIdentityMatches,
type ElfLibraryIdentity,
type ElfDependencies,
} from "./elfDependencies.js";
import {
installableLibraryPackages,
libraryClosure,
@@ -27,18 +31,47 @@ const elf = (
): ElfDependencies => ({
machine: 183,
bits: 64,
byte_order: "little",
needed,
runpath,
rpath: runpath === undefined ? rpath : [],
});
const TYPORA = "/usr/share/typora/Typora";
const app = (dependencies: ElfDependencies) => ({
path: "/opt/app/bin/app",
dependencies,
});
/** Typora's start: libffmpeg.so from its own folder through RPATH $ORIGIN, the rest from the cache. */
const host = (images: Readonly<Record<string, ElfDependencies | string>>) => ({
dependencies: async (path: string) => {
const host = (
images: Readonly<
Record<
string,
| ElfDependencies
| { identity: ElfLibraryIdentity | null; reason: string }
| string
>
>,
) => ({
dependencies: async (path: string, expected: ElfLibraryIdentity) => {
const image = images[path];
return typeof image === "object" ? ok(image) : err(image ?? "missing");
if (typeof image !== "object")
return err({
kind: "unreadable" as const,
identity: null,
reason: image ?? "missing",
});
const identity = "reason" in image ? image.identity : image;
if (identity !== null && !elfLibraryIdentityMatches(expected, identity))
return err({
kind: "incompatible" as const,
identity,
reason: "incompatible ELF",
});
return "reason" in image
? err({ kind: "unreadable" as const, ...image })
: ok(image);
},
isFile: async (path: string) => path in images,
});
@@ -102,7 +135,10 @@ describe("libraryClosure", () => {
parseLinkerCache(LINKER_CACHE),
host({
"/usr/share/typora/libffmpeg.so": elf([]),
"/lib/aarch64-linux-gnu/libnss3.so": "the file could not be read",
"/lib/aarch64-linux-gnu/libnss3.so": {
identity: elf([]),
reason: "the file could not be read",
},
}),
2,
);
@@ -120,12 +156,175 @@ describe("libraryClosure", () => {
});
});
describe("libraryClosure search paths", () => {
const app = (dependencies: ElfDependencies) => ({
path: "/opt/app/bin/app",
dependencies,
describe("libraryClosure candidate identity", () => {
it.each([
{ machine: 62, bits: 64 as const },
{ machine: 183, bits: 32 as const },
{ machine: 183, bits: 64 as const, byte_order: "big" as const },
])(
"skips an incompatible search-path candidate ($machine / $bits) before selecting its owner or walking its dependencies",
async (identity) => {
const closure = await libraryClosure(
app(elf(["libcore.so"], [], ["/foreign", "/native"])),
[],
host({
"/foreign/libcore.so": { ...elf(["libdecoy.so"]), ...identity },
"/native/libcore.so": elf(["libhelper.so"], [], ["/native"]),
"/native/libhelper.so": elf([]),
}),
10,
);
expect(closure).toEqual({
libraries: [
{ soname: "libcore.so", path: "/native/libcore.so" },
{ soname: "libhelper.so", path: "/native/libhelper.so" },
],
missing: [],
unreadable: [],
truncated: false,
});
},
);
it("uses the cache after an incompatible candidate even when its dependency table is unreadable", async () => {
const closure = await libraryClosure(
app(elf(["libcore.so"], ["/foreign"])),
[
{
soname: "libcore.so",
path: "/cached/libcore.so",
flags: ["AArch64"],
},
],
host({
"/foreign/libcore.so": {
identity: { machine: 62, bits: 64, byte_order: "little" },
reason: "bad dynamic table",
},
"/cached/libcore.so": elf([]),
}),
10,
);
expect(closure).toEqual({
libraries: [{ soname: "libcore.so", path: "/cached/libcore.so" }],
missing: [],
unreadable: [],
truncated: false,
});
});
});
describe("libraryClosure unresolved candidates", () => {
it("does not turn unknown candidate identity into a selected library or search past it", async () => {
const closure = await libraryClosure(
app(elf(["libcore.so"], [], ["/unreadable", "/native"])),
[],
host({
"/unreadable/libcore.so": "cannot read header",
"/native/libcore.so": elf([]),
}),
10,
);
expect(closure).toEqual({
libraries: [],
missing: [],
unreadable: [
{ path: "/unreadable/libcore.so", reason: "cannot read header" },
],
truncated: false,
});
});
it("refuses an incompatible explicit path without claiming its package was selected", async () => {
const path = "/foreign/libcore.so";
const closure = await libraryClosure(
app(elf([path])),
[],
host({ [path]: { ...elf([]), machine: 62 } }),
10,
);
expect(closure).toMatchObject({
libraries: [],
missing: [],
unreadable: [
{ path, reason: expect.stringContaining("incompatible ELF") },
],
truncated: false,
});
});
it("continues to another matching cache candidate after foreign identity", async () => {
const closure = await libraryClosure(
app(elf(["libcore.so"])),
[
{
soname: "libcore.so",
path: "/foreign/libcore.so",
flags: ["AArch64"],
},
{ soname: "libcore.so", path: "/valid/libcore.so", flags: ["AArch64"] },
],
host({
"/foreign/libcore.so": { ...elf([]), machine: 62 },
"/valid/libcore.so": elf([]),
}),
10,
);
expect(closure).toMatchObject({
libraries: [{ soname: "libcore.so", path: "/valid/libcore.so" }],
missing: [],
});
});
it("keeps an unreadable first cache candidate unresolved", async () => {
const closure = await libraryClosure(
app(elf(["libcore.so"])),
[
{
soname: "libcore.so",
path: "/unknown/libcore.so",
flags: ["AArch64"],
},
{ soname: "libcore.so", path: "/valid/libcore.so", flags: ["AArch64"] },
],
host({
"/unknown/libcore.so": "cannot read header",
"/valid/libcore.so": elf([]),
}),
10,
);
expect(closure).toMatchObject({
libraries: [],
missing: [],
unreadable: [
{ path: "/unknown/libcore.so", reason: "cannot read header" },
],
});
});
it("does not trust a cache architecture flag over the candidate's own ELF identity", async () => {
const closure = await libraryClosure(
app(elf(["libcore.so"])),
[
{
soname: "libcore.so",
path: "/cached/libcore.so",
flags: ["AArch64"],
},
],
host({ "/cached/libcore.so": { ...elf([]), machine: 62 } }),
10,
);
expect(closure).toEqual({
libraries: [],
missing: ["libcore.so"],
unreadable: [],
truncated: false,
});
});
});
describe("libraryClosure search paths", () => {
it("normalizes $ORIGIN/.. before telling the app's own folder from system libraries", async () => {
const closure = await libraryClosure(
{
+83 -23
View File
@@ -3,7 +3,11 @@ import { posix } from "node:path";
import type { Result } from "../result.js";
import { fromUbuntuNobleArchive } from "./appExportInstall.js";
import { isWithinAny } from "./desktopStateDiff.js";
import type { ElfDependencies } from "./elfDependencies.js";
import type {
ElfDependencies,
ElfDependenciesFailure,
ElfLibraryIdentity,
} from "./elfDependencies.js";
/** One library `ldconfig -p` lists from the dynamic linker cache. */
export interface LinkerCacheEntry {
@@ -36,20 +40,23 @@ const CACHE_FLAGS: ReadonlyMap<number, string> = new Map([
]);
const MACHINE_FLAGS = new Set(CACHE_FLAGS.values());
const cachedPath = (
const cachedPaths = (
cache: readonly LinkerCacheEntry[],
soname: string,
machine: number,
): string | undefined => {
): readonly string[] => {
const matching = cache.filter((entry) => entry.soname === soname);
const flag = CACHE_FLAGS.get(machine);
return (
flag === undefined
? (matching.find(
(entry) => !entry.flags.some((name) => MACHINE_FLAGS.has(name)),
) ?? matching[0])
: matching.find((entry) => entry.flags.includes(flag))
)?.path;
if (flag !== undefined)
return matching
.filter((entry) => entry.flags.includes(flag))
.map((entry) => entry.path);
const unclassified = matching.filter(
(entry) => !entry.flags.some((name) => MACHINE_FLAGS.has(name)),
);
return (unclassified.length > 0 ? unclassified : matching).map(
(entry) => entry.path,
);
};
/** A loaded object and the object whose DT_NEEDED loaded it, as ld.so links them. */
@@ -89,11 +96,32 @@ const searchDirectories = (object: LoadedObject): readonly string[] => {
];
};
/** Search order identifies candidates; existence alone does not select a loaded object. */
async function* libraryCandidates(
soname: string,
directories: readonly string[],
machine: number,
cache: readonly LinkerCacheEntry[],
host: LibraryWalkHost,
): AsyncGenerator<string> {
if (soname.startsWith("/")) {
yield posix.normalize(soname);
return;
}
if (!soname.includes("/"))
for (const directory of directories) {
const candidate = posix.join(directory, soname);
if (await host.isFile(candidate)) yield candidate;
}
yield* cachedPaths(cache, soname, machine);
}
/** Host reads the closure walk needs. */
export interface LibraryWalkHost {
readonly dependencies: (
path: string,
) => Promise<Result<ElfDependencies, string>>;
expected: ElfLibraryIdentity,
) => Promise<Result<ElfDependencies, ElfDependenciesFailure>>;
readonly isFile: (path: string) => Promise<boolean>;
}
@@ -104,9 +132,9 @@ export interface LibraryClosure {
readonly soname: string;
readonly path: string;
}[];
/** Sonames found neither in an RPATH or RUNPATH directory nor in the linker cache. */
/** Sonames without a compatible candidate in the searched directories or linker cache. */
readonly missing: readonly string[];
/** Libraries whose own dependencies could not be read. */
/** Candidates whose selection or dependency closure could not be established. */
readonly unreadable: readonly {
readonly path: string;
readonly reason: string;
@@ -144,23 +172,55 @@ export const libraryClosure = async (
if (seen.has(soname)) continue;
if (seen.size >= limit) return { ...closure, truncated: true };
seen.add(soname);
let path = soname.startsWith("/") ? posix.normalize(soname) : undefined;
for (const directory of soname.includes("/") ? [] : directories) {
const candidate = posix.join(directory, soname);
if (path === undefined && (await host.isFile(candidate)))
path = candidate;
let selected:
| {
path: string;
dependencies: Result<ElfDependencies, ElfDependenciesFailure>;
}
| undefined;
let unresolved = false;
for await (const path of libraryCandidates(
soname,
directories,
object.dependencies.machine,
cache,
host,
)) {
const dependencies = await host.dependencies(path, object.dependencies);
if (!dependencies.ok && dependencies.error.identity === null) {
// A failed identity read does not prove this candidate can be skipped,
// or that its package belongs to the executable's loaded closure.
closure.unreadable.push({
path,
reason: dependencies.error.reason,
});
unresolved = true;
break;
}
if (!dependencies.ok && dependencies.error.kind === "incompatible") {
if (soname.startsWith("/")) {
closure.unreadable.push({
path,
reason: dependencies.error.reason,
});
unresolved = true;
break;
}
continue;
}
selected = { path, dependencies };
break;
}
path ??= cachedPath(cache, soname, object.dependencies.machine);
if (path === undefined) {
closure.missing.push(soname);
if (selected === undefined) {
if (!unresolved) closure.missing.push(soname);
continue;
}
const { path, dependencies } = selected;
if (!isWithinAny(path, [ownFolder]))
closure.libraries.push({ soname, path });
const dependencies = await host.dependencies(path);
if (dependencies.ok)
queue.push({ path, dependencies: dependencies.value, loader: object });
else closure.unreadable.push({ path, reason: dependencies.error });
else closure.unreadable.push({ path, reason: dependencies.error.reason });
}
}
return closure;
+10 -2
View File
@@ -85,8 +85,16 @@ export const readElfFixedHeader = (
totalBytes = bytes.length,
): Result<ElfFixedHeader, string> => {
const parsed = readElfHeaderFacts(bytes, totalBytes);
if (!parsed.ok) return parsed;
const facts = parsed.value;
return parsed.ok
? admitElfFixedHeaderFacts(parsed.value, totalBytes)
: parsed;
};
/** Admit decoded facts without rereading or discarding their identity on failure. */
export const admitElfFixedHeaderFacts = (
facts: ElfHeaderFacts,
totalBytes: number,
): Result<ElfFixedHeader, string> => {
// NVIDIA CUBIN uses ELFOSABI_CUDA and a vendor ABI version in e_version.
if (facts.version !== 1 && !(facts.machine === 190 && facts.osAbi === 0x33))
return err("unsupported ELF header version");