fix(artifacts): preserve Mach-O load-command string identity (#2110)
* fix(artifacts): preserve Mach-O load-command string identity
* fix(process): report no remaining time after a provider deadline fires
Node timers can fire while performance.now() is still a fraction of a
millisecond short of the monotonic deadline. ProviderStartupDeadline then
reported interruption "timeout" and an aborted signal while remainingMs()
returned ceil(0.x) = 1, so polling loops guarded by remainingMs() > 0
(Hopper and Ghidra startup) could run past an elapsed deadline, and the
real-timer test "aborts a bounded wait when its real monotonic deadline
expires" failed intermittently in CI (expected 1 to be 0).
Once the deadline's own timer has classified a timeout, report zero
remaining time. A deterministic regression test fires the timer with the
monotonic clock 0.4 ms short of the deadline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit dfdaa0ca74)
* fix(ci): isolate Linux Hopper process fixture inventory
* fix(comparison): recheck historical calls-only changes (#2223)
* fix(comparison): preserve incomplete function call evidence
Keep missing, unknown, partial and unresolved call relationships unknown in function comparisons and reconstruction claims. Aggregate IDA caller and callee coverage without overwriting uncertainty.
Validate with 144 focused tests, typecheck/lint, docs checks and test discovery. Real providers were not verified; the full headless IDA session suite requires unavailable Windows native controls.
* fix(comparison): recheck historical calls-only changes
---------
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
* style: format identity comparison and IDA runner
---------
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Lucien <164857476+dakjdakd@users.noreply.github.com>
This commit is contained in:
3 files changed
+74
-1
No files matched your search
@@ -229,6 +229,27 @@ it("selects matching arm64e subtype slices when tracing a mixed FAT image", asyn
|
||||
});
|
||||
|
||||
describe("Mach-O load command reader: decodes thin-image load commands", () => {
|
||||
it("preserves leading U+FEFF in load-command strings", async () => {
|
||||
const image = machoImage({
|
||||
commands: [
|
||||
dylibCommand(LC.ID_DYLIB, "\uFEFF@rpath/libself.dylib"),
|
||||
dylibCommand(LC.LOAD_DYLIB, "\uFEFF@rpath/libother.dylib"),
|
||||
rpathCommand("\uFEFF@loader_path/Frameworks"),
|
||||
dyldEnvironmentCommand("\uFEFFDYLD_LIBRARY_PATH=/example"),
|
||||
],
|
||||
});
|
||||
expect(await read(image)).toMatchObject({
|
||||
status: "parsed",
|
||||
slices: [
|
||||
{
|
||||
install_name: "\uFEFF@rpath/libself.dylib",
|
||||
dependencies: [{ install_name: "\uFEFF@rpath/libother.dylib" }],
|
||||
rpaths: ["\uFEFF@loader_path/Frameworks"],
|
||||
dyld_environment: ["\uFEFFDYLD_LIBRARY_PATH=/example"],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
it("decodes dylib loading commands of a thin image", async () => {
|
||||
const image = machoImage({
|
||||
fileType: FILE_TYPE.dylib,
|
||||
|
||||
@@ -456,7 +456,8 @@ const commandString = (
|
||||
`load command ${index} has an unterminated string`,
|
||||
);
|
||||
try {
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
// lc_str contains name/path data, so a leading U+FEFF is part of its identity.
|
||||
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
body.subarray(offset, end),
|
||||
);
|
||||
} catch {
|
||||
|
||||
@@ -106,6 +106,57 @@ const withBrokenHelper = async (app: string): Promise<void> => {
|
||||
await writeFiles(app, { "Contents/Helpers/broken": broken });
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"does not turn U+FEFF-prefixed strings into dyld path tokens",
|
||||
async ({ cli }) => {
|
||||
const app = await fixtureApp();
|
||||
await writeFiles(app, {
|
||||
"Contents/MacOS/App": machoImage({
|
||||
commands: [
|
||||
buildVersionCommand(1),
|
||||
rpathCommand("@executable_path/../Frameworks"),
|
||||
dylibCommand(LC.LOAD_DYLIB, "@rpath/libcore.dylib"),
|
||||
dylibCommand(LC.LOAD_DYLIB, "\uFEFF@rpath/libcore.dylib"),
|
||||
],
|
||||
}),
|
||||
"Contents/Helpers/tool": machoImage({
|
||||
commands: [
|
||||
buildVersionCommand(1),
|
||||
rpathCommand("\uFEFF@loader_path/../Frameworks"),
|
||||
dylibCommand(LC.LOAD_DYLIB, "@rpath/libcore.dylib"),
|
||||
],
|
||||
}),
|
||||
});
|
||||
const output = await cli.run({
|
||||
arguments: ["trace-dylib-resolution", app, "--json"],
|
||||
environment: ENVIRONMENT,
|
||||
});
|
||||
expect(output.exitCode, output.stderr).toBe(0);
|
||||
const trace = dylibResolutionResultSchema.parse(
|
||||
parseEvidence(output.json).normalized_result,
|
||||
);
|
||||
const ordinary = trace.edges.find(
|
||||
({ loader, install_name }) =>
|
||||
loader === "Contents/MacOS/App" &&
|
||||
install_name === "@rpath/libcore.dylib",
|
||||
);
|
||||
expect(ordinary?.resolution).toMatchObject({
|
||||
status: "resolved",
|
||||
image: "Contents/Frameworks/libcore.dylib",
|
||||
});
|
||||
const marked = trace.edges.find(
|
||||
({ install_name }) => install_name === "\uFEFF@rpath/libcore.dylib",
|
||||
);
|
||||
expect(marked).toBeDefined();
|
||||
expect(marked?.resolution.image).toBeNull();
|
||||
const helper = trace.edges.find(
|
||||
({ loader }) => loader === "Contents/Helpers/tool",
|
||||
);
|
||||
expect(helper).toBeDefined();
|
||||
expect(helper?.resolution.image).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
describe("trace-dylib-resolution CLI", () => {
|
||||
cliTest("traces every executable root of an app bundle", async ({ cli }) => {
|
||||
const app = await fixtureApp();
|
||||
|
||||
Reference in new issue
Block a user