fix(targets): cancel resolution before provider admission
This commit is contained in:
parent
44c92f88c5
commit
f3c4a332f7
|
|
@ -51,11 +51,10 @@ export async function verifyLegacyGhidraReferenceSnapshot(
|
|||
legacyParameters,
|
||||
);
|
||||
assert.notEqual(legacyProfile.digest, observed.analysis_profile.digest);
|
||||
const parsed = await parseBinaryTarget(
|
||||
target.path,
|
||||
process.cwd(),
|
||||
process.arch,
|
||||
);
|
||||
const parsed = await parseBinaryTarget(target.path, {
|
||||
cwd: process.cwd(),
|
||||
hostArchitecture: process.arch,
|
||||
});
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
const nativeTarget = parsed.value;
|
||||
const result = structuredClone(observed.normalized_result);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { extname, isAbsolute, resolve } from "node:path";
|
||||
|
||||
import { BinaryTargetError } from "../domain/configurationErrors.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
|
|
@ -32,6 +33,15 @@ import {
|
|||
type ExecutableMetadata,
|
||||
} from "../domain/binaryTarget.js";
|
||||
|
||||
/** Options for resolving a local binary target and its stable identity. */
|
||||
export interface BinaryTargetResolverOptions {
|
||||
readonly cwd?: string;
|
||||
readonly hostArchitecture?: NodeJS.Architecture;
|
||||
readonly targetKind?: BinaryTarget["kind"];
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and classify a readable local target before a provider is selected.
|
||||
* FAT Mach-O inputs select only a host-compatible architecture so setup remains
|
||||
|
|
@ -39,24 +49,32 @@ import {
|
|||
*/
|
||||
export const parseBinaryTarget = async (
|
||||
input: string,
|
||||
cwd = process.cwd(),
|
||||
hostArchitecture: NodeJS.Architecture = process.arch,
|
||||
targetKind?: BinaryTarget["kind"],
|
||||
formatHint?: ExecutableFormatHint,
|
||||
): Promise<Result<BinaryTarget, BinaryTargetError>> => {
|
||||
options: BinaryTargetResolverOptions = {},
|
||||
): Promise<
|
||||
Result<BinaryTarget, BinaryTargetError | AnalysisCancelledError>
|
||||
> => {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const hostArchitecture = options.hostArchitecture ?? process.arch;
|
||||
const { targetKind, formatHint, signal } = options;
|
||||
const candidate = isAbsolute(input) ? input : resolve(cwd, input);
|
||||
try {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
await access(candidate, constants.R_OK);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const canonical = await realpath(candidate);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const resolved = await resolveAppBundle(canonical);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (!resolved.ok) return err(resolved.error);
|
||||
const { executable: path, infoPlist } = resolved.value;
|
||||
const bundle =
|
||||
infoPlist === undefined ? {} : { bundleInfoPlist: infoPlist };
|
||||
const handle = await open(path, "r");
|
||||
try {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (!(await handle.stat()).isFile())
|
||||
return err(new BinaryTargetError(path, "target is not a regular file"));
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (formatHint === "dos-com") {
|
||||
if (targetKind !== undefined && targetKind !== "executable")
|
||||
return err(
|
||||
|
|
@ -70,7 +88,7 @@ export const parseBinaryTarget = async (
|
|||
return ok({
|
||||
path,
|
||||
sourcePath: canonical,
|
||||
sha256: await sha256Handle(handle),
|
||||
sha256: await sha256Handle(handle, signal),
|
||||
kind: "executable",
|
||||
format: "dos-com",
|
||||
architecture: "x86",
|
||||
|
|
@ -84,28 +102,32 @@ export const parseBinaryTarget = async (
|
|||
return ok({
|
||||
path,
|
||||
sourcePath: process.platform === "win32" ? candidate : canonical,
|
||||
sha256: await sha256Handle(handle),
|
||||
sha256: await sha256Handle(handle, signal),
|
||||
kind: "database",
|
||||
format: "analysis-database",
|
||||
});
|
||||
const artifactFormat = await detectArtifactFormat(path, handle);
|
||||
const artifactFormat = await detectArtifactFormat(path, handle, signal);
|
||||
if (artifactFormat !== undefined) {
|
||||
const identity = {
|
||||
path,
|
||||
sourcePath: process.platform === "win32" ? candidate : canonical,
|
||||
sha256: await sha256Handle(handle),
|
||||
sha256: await sha256Handle(handle, signal),
|
||||
};
|
||||
return isArchiveFormat(artifactFormat)
|
||||
? ok({ ...identity, kind: "archive", format: artifactFormat })
|
||||
: ok({ ...identity, kind: "artifact", format: artifactFormat });
|
||||
}
|
||||
const detected = await readExecutableMetadata(handle, hostArchitecture);
|
||||
const detected = await readExecutableMetadata(
|
||||
handle,
|
||||
hostArchitecture,
|
||||
signal,
|
||||
);
|
||||
if (!detected.ok) return err(new BinaryTargetError(path, detected.error));
|
||||
return ok({
|
||||
path,
|
||||
sourcePath: process.platform === "win32" ? candidate : canonical,
|
||||
...bundle,
|
||||
sha256: await sha256Handle(handle),
|
||||
sha256: await sha256Handle(handle, signal),
|
||||
kind: "executable",
|
||||
...detected.value,
|
||||
});
|
||||
|
|
@ -113,6 +135,7 @@ export const parseBinaryTarget = async (
|
|||
await handle.close();
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof AnalysisCancelledError) return err(cause);
|
||||
if (!(cause instanceof Error)) throw cause;
|
||||
const code = filesystemErrorCode(cause);
|
||||
if (code === undefined) throw cause;
|
||||
|
|
@ -146,9 +169,15 @@ const targetFailureReason = (code: string, detail: string): string => {
|
|||
}
|
||||
};
|
||||
|
||||
const throwIfTargetResolutionCancelled = (signal?: AbortSignal): void => {
|
||||
if (signal?.aborted === true)
|
||||
throw new AnalysisCancelledError("resolve_binary_target");
|
||||
};
|
||||
|
||||
const detectArtifactFormat = async (
|
||||
path: string,
|
||||
handle: FileHandle,
|
||||
signal?: AbortSignal,
|
||||
): Promise<
|
||||
| Exclude<
|
||||
BinaryTarget["format"],
|
||||
|
|
@ -156,9 +185,11 @@ const detectArtifactFormat = async (
|
|||
>
|
||||
| undefined
|
||||
> => {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const lower = path.toLowerCase();
|
||||
const magic = Buffer.alloc(8);
|
||||
const observed = await handle.read(magic, 0, magic.length, 0);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (hasZipSignature(magic.subarray(0, observed.bytesRead))) {
|
||||
return zipPackageFormatForPath(lower) ?? "zip";
|
||||
}
|
||||
|
|
@ -171,10 +202,13 @@ const detectArtifactFormat = async (
|
|||
)
|
||||
return "pkg";
|
||||
if (lower.endsWith(".dmg")) {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const size = (await handle.stat()).size;
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (size >= 512) {
|
||||
const trailer = Buffer.alloc(4);
|
||||
const read = await handle.read(trailer, 0, trailer.length, size - 512);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (read.bytesRead === 4 && trailer.toString("ascii") === "koly")
|
||||
return "dmg";
|
||||
}
|
||||
|
|
@ -199,12 +233,17 @@ const isArchiveFormat = (
|
|||
): format is Extract<BinaryTarget, { kind: "archive" }>["format"] =>
|
||||
["zip", "ipa", "apk", "msix", "appx", "asar", "dmg", "pkg"].includes(format);
|
||||
|
||||
const sha256Handle = async (handle: FileHandle): Promise<string> => {
|
||||
const sha256Handle = async (
|
||||
handle: FileHandle,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> => {
|
||||
const hash = createHash("sha256");
|
||||
const chunk = Buffer.allocUnsafe(64 * 1024);
|
||||
let position = 0;
|
||||
while (true) {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const observed = await handle.read(chunk, 0, chunk.length, position);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (observed.bytesRead === 0) break;
|
||||
hash.update(chunk.subarray(0, observed.bytesRead));
|
||||
position += observed.bytesRead;
|
||||
|
|
@ -233,19 +272,23 @@ const resolveAppBundle = async (
|
|||
const readExecutableMetadata = async (
|
||||
handle: FileHandle,
|
||||
hostArchitecture: NodeJS.Architecture,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<ExecutableMetadata, string>> => {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const prefix = Buffer.alloc(4096);
|
||||
const prefixRead = await handle.read(prefix, 0, prefix.length, 0);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const bytes = prefix.subarray(0, prefixRead.bytesRead);
|
||||
// Header commitments such as Mach-O load commands and FAT slices are
|
||||
// checked against the whole file, not the probed prefix.
|
||||
const fileSize = (await handle.stat()).size;
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a) {
|
||||
const offset = mzWindowsHeaderOffset(bytes);
|
||||
if (offset !== null) {
|
||||
if (offset < 64)
|
||||
return err("invalid Windows new-header offset in MZ image");
|
||||
return readPeMetadata(handle, offset);
|
||||
return readPeMetadata(handle, offset, signal);
|
||||
}
|
||||
// MZ relocation records may extend past the initial probe; their format
|
||||
// bounds are at most 65535 records inside a 65535-paragraph header.
|
||||
|
|
@ -261,6 +304,7 @@ const readExecutableMetadata = async (
|
|||
) {
|
||||
const table = Buffer.alloc(tableEnd);
|
||||
const observed = await handle.read(table, 0, table.length, 0);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
return parseExecutableHeader(
|
||||
table.subarray(0, observed.bytesRead),
|
||||
hostArchitecture,
|
||||
|
|
@ -278,6 +322,7 @@ const readExecutableMetadata = async (
|
|||
if (count <= 128 && required > bytes.length) {
|
||||
const header = Buffer.alloc(required);
|
||||
const headerRead = await handle.read(header, 0, header.length, 0);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
return parseExecutableHeader(
|
||||
header.subarray(0, headerRead.bytesRead),
|
||||
hostArchitecture,
|
||||
|
|
@ -292,7 +337,9 @@ const readExecutableMetadata = async (
|
|||
const readPeMetadata = async (
|
||||
handle: FileHandle,
|
||||
offset: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<ExecutableMetadata, string>> => {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const fileHeader = Buffer.alloc(24);
|
||||
const fileHeaderRead = await handle.read(
|
||||
fileHeader,
|
||||
|
|
@ -300,6 +347,7 @@ const readPeMetadata = async (
|
|||
fileHeader.length,
|
||||
offset,
|
||||
);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (fileHeaderRead.bytesRead !== fileHeader.length)
|
||||
return err("invalid or truncated PE header");
|
||||
const signature = fileHeader.toString("ascii", 0, 2);
|
||||
|
|
@ -309,6 +357,7 @@ const readPeMetadata = async (
|
|||
if (optionalHeaderSize > 4096) return err("invalid PE optional header size");
|
||||
const record = Buffer.alloc(24 + optionalHeaderSize);
|
||||
const recordRead = await handle.read(record, 0, record.length, offset);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
return recordRead.bytesRead === record.length
|
||||
? parsePeRecord(record)
|
||||
: err("invalid or truncated PE header");
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ const runAnalysis = async (
|
|||
const prepared = await prepareSnapshot({
|
||||
path,
|
||||
snapshotPath,
|
||||
signal,
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
: { formatHint: options.formatHint }),
|
||||
|
|
@ -434,6 +435,7 @@ const prepareSnapshot = async (options: {
|
|||
readonly path: string;
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly snapshotPath: string | undefined;
|
||||
readonly signal: AbortSignal;
|
||||
}): Promise<
|
||||
Result<
|
||||
{ readonly snapshot?: AnalysisSnapshot; readonly target?: BinaryTarget },
|
||||
|
|
@ -445,13 +447,12 @@ const prepareSnapshot = async (options: {
|
|||
return ok({});
|
||||
const loaded = await readAnalysisSnapshot(snapshotPath);
|
||||
if (!loaded.ok) return loaded;
|
||||
const target = await parseBinaryTarget(
|
||||
path,
|
||||
process.cwd(),
|
||||
process.arch,
|
||||
undefined,
|
||||
options.formatHint,
|
||||
);
|
||||
const target = await parseBinaryTarget(path, {
|
||||
signal: options.signal,
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
: { formatHint: options.formatHint }),
|
||||
});
|
||||
if (!target.ok) return target;
|
||||
if (!snapshotMatchesTarget(loaded.value.target, target.value))
|
||||
return err(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
|
|||
import { join } from "node:path";
|
||||
|
||||
import { analysisErrorRemediationAction } from "../domain/analysisErrorPresentation.js";
|
||||
import { BinaryTargetError } from "../domain/configurationErrors.js";
|
||||
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
|
||||
import { execFileOutput } from "../process/ExecFileOutput.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
|
@ -324,10 +325,18 @@ export const systemDoctorHost = (
|
|||
readMacosVersion(hostExecFileOutput, commandEnvironment),
|
||||
linuxDistribution: readLinuxDistribution,
|
||||
async validTarget(path) {
|
||||
return (await parseBinaryTarget(path, process.cwd(), architecture)).ok;
|
||||
return (
|
||||
await parseBinaryTarget(path, {
|
||||
cwd: process.cwd(),
|
||||
hostArchitecture: architecture,
|
||||
})
|
||||
).ok;
|
||||
},
|
||||
async inspectTarget(path) {
|
||||
const result = await parseBinaryTarget(path, process.cwd(), architecture);
|
||||
const result = await parseBinaryTarget(path, {
|
||||
cwd: process.cwd(),
|
||||
hostArchitecture: architecture,
|
||||
});
|
||||
if (result.ok)
|
||||
return {
|
||||
name: "target",
|
||||
|
|
@ -336,19 +345,21 @@ export const systemDoctorHost = (
|
|||
detail: path,
|
||||
};
|
||||
const error = result.error;
|
||||
const targetError =
|
||||
error instanceof BinaryTargetError ? error : undefined;
|
||||
return {
|
||||
name: "target",
|
||||
ok: false,
|
||||
classification:
|
||||
error.constraint === "directory_requires_file"
|
||||
targetError?.constraint === "directory_requires_file"
|
||||
? "unsupported_target"
|
||||
: "config_drift",
|
||||
detail: path,
|
||||
details: {
|
||||
reason: error.reason,
|
||||
...(error.constraint === undefined
|
||||
reason: targetError?.reason ?? error.message,
|
||||
...(targetError?.constraint === undefined
|
||||
? {}
|
||||
: { constraint: error.constraint }),
|
||||
: { constraint: targetError.constraint }),
|
||||
},
|
||||
remediation: analysisErrorRemediationAction(error),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ export class AndroidAnalysisService {
|
|||
const request = androidRequestSchema.safeParse({ operation, input });
|
||||
if (!request.success)
|
||||
return err(new AnalysisInputError(operation, { cause: request.error }));
|
||||
const target = await parseBinaryTarget(request.data.input.path);
|
||||
const target = await parseBinaryTarget(request.data.input.path, {
|
||||
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
||||
});
|
||||
if (!target.ok) return target;
|
||||
const executed = await this.provider.execute(
|
||||
target.value,
|
||||
|
|
|
|||
|
|
@ -65,13 +65,15 @@ export const resolveSessionOpen = async (
|
|||
input: ResolveSessionOpenInput,
|
||||
): Promise<Result<ResolvedSessionOpen, AnalysisError>> => {
|
||||
const { path, options } = input;
|
||||
const parsed = await parseBinaryTarget(
|
||||
path,
|
||||
process.cwd(),
|
||||
process.arch,
|
||||
options.targetKind,
|
||||
options.formatHint,
|
||||
);
|
||||
const parsed = await parseBinaryTarget(path, {
|
||||
...(options.targetKind === undefined
|
||||
? {}
|
||||
: { targetKind: options.targetKind }),
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
: { formatHint: options.formatHint }),
|
||||
...(options.signal === undefined ? {} : { signal: options.signal }),
|
||||
});
|
||||
if (!parsed.ok) return parsed;
|
||||
return resolveSessionTarget({ ...input, target: parsed.value });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -226,12 +226,16 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
|
|||
const directory = await createTestTempDirectory("rea-target-");
|
||||
await writeFile(join(directory, "sample.hop"), "database");
|
||||
await writeFile(join(directory, "text"), "hello");
|
||||
const database = await parseBinaryTarget("sample.hop", directory);
|
||||
const database = await parseBinaryTarget("sample.hop", { cwd: directory });
|
||||
expect(database.ok && database.value.sha256).toBe(
|
||||
"3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8",
|
||||
);
|
||||
expect((await parseBinaryTarget("text", directory)).ok).toBe(false);
|
||||
expect((await parseBinaryTarget("missing", directory)).ok).toBe(false);
|
||||
expect((await parseBinaryTarget("text", { cwd: directory })).ok).toBe(
|
||||
false,
|
||||
);
|
||||
expect((await parseBinaryTarget("missing", { cwd: directory })).ok).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
|
||||
|
|
@ -242,7 +246,10 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
|
|||
await writeFile(path, thinMach(0xcffaedfe, 0x0100000c));
|
||||
await chmod(path, 0);
|
||||
try {
|
||||
const result = await parseBinaryTarget(path, directory, "arm64");
|
||||
const result = await parseBinaryTarget(path, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error).toMatchObject({ path, systemCode: "EACCES" });
|
||||
|
|
@ -275,7 +282,10 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
|
|||
join(programs, "Example & Tool"),
|
||||
thinMach(0xfeedfacf, 0x0100000c),
|
||||
);
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(join(programs, "Example & Tool")),
|
||||
format: "mach-o",
|
||||
|
|
@ -300,7 +310,14 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
|
|||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
if (plist !== undefined)
|
||||
await writeFile(join(contents, "Info.plist"), plist);
|
||||
expect((await parseBinaryTarget(app, directory, "arm64")).ok).toBe(false);
|
||||
expect(
|
||||
(
|
||||
await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
})
|
||||
).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
|
|
@ -318,7 +335,14 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
|
|||
"<plist><dict><key>CFBundleExecutable</key><string>Escaping</string></dict></plist>",
|
||||
);
|
||||
await symlink(outside, join(programs, "Escaping"));
|
||||
expect((await parseBinaryTarget(app, directory, "arm64")).ok).toBe(false);
|
||||
expect(
|
||||
(
|
||||
await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
})
|
||||
).ok,
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -327,12 +351,11 @@ describe("binary target I/O: explicit kinds and executable headers", () => {
|
|||
it("honors an explicit database kind without relying on the file suffix", async () => {
|
||||
const directory = await createTestTempDirectory("rea-target-");
|
||||
await writeFile(join(directory, "saved-analysis"), "database");
|
||||
const result = await parseBinaryTarget(
|
||||
"saved-analysis",
|
||||
directory,
|
||||
"arm64",
|
||||
"database",
|
||||
);
|
||||
const result = await parseBinaryTarget("saved-analysis", {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
targetKind: "database",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
kind: "database",
|
||||
format: "analysis-database",
|
||||
|
|
@ -343,7 +366,10 @@ describe("binary target I/O: explicit kinds and executable headers", () => {
|
|||
const directory = await createTestTempDirectory("rea-target-");
|
||||
const path = join(directory, "delayed.exe");
|
||||
await writeFile(path, pe(0x8664, 8192));
|
||||
const result = await parseBinaryTarget(path, directory, "x64");
|
||||
const result = await parseBinaryTarget(path, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "x64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
format: "pe",
|
||||
architecture: "x86_64",
|
||||
|
|
@ -354,7 +380,10 @@ describe("binary target I/O: explicit kinds and executable headers", () => {
|
|||
const directory = await createTestTempDirectory("rea-target-");
|
||||
const path = join(directory, "truncated");
|
||||
await writeFile(path, thinMach(0xcffaedfe, 0x0100000c).subarray(0, 12));
|
||||
const result = await parseBinaryTarget(path, directory, "arm64");
|
||||
const result = await parseBinaryTarget(path, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.ok ? undefined : result.error).toMatchObject({
|
||||
path,
|
||||
|
|
@ -369,7 +398,10 @@ describe("binary target I/O: explicit kinds and executable headers", () => {
|
|||
const header = thinMach(0xcffaedfe, 0x0100000c);
|
||||
header.writeUInt32LE(8192, 20);
|
||||
await writeFile(path, Buffer.concat([header, Buffer.alloc(8192)]));
|
||||
const result = await parseBinaryTarget(path, directory, "arm64");
|
||||
const result = await parseBinaryTarget(path, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
format: "mach-o",
|
||||
architecture: "arm64",
|
||||
|
|
@ -391,7 +423,10 @@ describe("app executable filename fidelity", () => {
|
|||
`<plist><dict><key>CFBundleExecutable</key><string>${name}</string></dict></plist>`,
|
||||
);
|
||||
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(executable),
|
||||
format: "mach-o",
|
||||
|
|
@ -416,7 +451,10 @@ describe("app executable filename fidelity", () => {
|
|||
`<plist><dict><key>CFBundleExecutable</key><string>${encoded}</string></dict></plist>`,
|
||||
);
|
||||
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(executable),
|
||||
format: "mach-o",
|
||||
|
|
@ -449,7 +487,10 @@ describe("app executable filename fidelity", () => {
|
|||
join(contents, "MacOS", "Inner"),
|
||||
thinMach(0xfeedfacf, 0x0100000c),
|
||||
);
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(executable),
|
||||
});
|
||||
|
|
@ -469,7 +510,10 @@ describe("app executable filename fidelity", () => {
|
|||
`<plist><dict>${entry}<key>CFBundleExecutable</key><string>App</string></dict></plist>`,
|
||||
);
|
||||
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(executable),
|
||||
});
|
||||
|
|
@ -495,7 +539,10 @@ describe("app executable filename fidelity", () => {
|
|||
plist,
|
||||
]);
|
||||
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(executable),
|
||||
format: "mach-o",
|
||||
|
|
@ -515,7 +562,10 @@ describe("iOS-style app bundle targets", () => {
|
|||
await mkdir(app);
|
||||
await writeFile(join(app, "Info.plist"), plist("Flat"));
|
||||
await writeFile(join(app, "Flat"), thinMach(0xfeedfacf, 0x0100000c));
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(join(app, "Flat")),
|
||||
bundleInfoPlist: join(await realpath(app), "Info.plist"),
|
||||
|
|
@ -535,7 +585,10 @@ describe("iOS-style app bundle targets", () => {
|
|||
thinMach(0xfeedfacf, 0x0100000c),
|
||||
);
|
||||
await symlink("bin/real", join(app, "Linked"));
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(join(app, "bin", "real")),
|
||||
bundleInfoPlist: join(await realpath(app), "Info.plist"),
|
||||
|
|
@ -556,7 +609,10 @@ describe("iOS-style app bundle targets", () => {
|
|||
join(contents, "MacOS", "Linked"),
|
||||
thinMach(0xfeedfacf, 0x0100000c),
|
||||
);
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(join(contents, "MacOS", "Linked")),
|
||||
});
|
||||
|
|
@ -572,7 +628,10 @@ describe("iOS-style app bundle targets", () => {
|
|||
await mkdir(join(wrapper, "Locked.app"), { recursive: true });
|
||||
await chmod(wrapper, 0o000);
|
||||
try {
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
if (result.ok) throw new Error("Expected a permission denial");
|
||||
expect(result.error.message).toContain("permission denied");
|
||||
} finally {
|
||||
|
|
@ -591,14 +650,24 @@ describe("iOS-style app bundle targets", () => {
|
|||
await writeFile(join(wrapped, "Info.plist"), plist("Phone"));
|
||||
await writeFile(join(wrapped, "Phone"), thinMach(0xfeedfacf, 0x0100000c));
|
||||
await symlink("Wrapper/Phone.app", join(app, "WrappedBundle"));
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(join(wrapped, "Phone")),
|
||||
format: "mach-o",
|
||||
});
|
||||
|
||||
await mkdir(join(app, "Wrapper", "Second.app"));
|
||||
expect((await parseBinaryTarget(app, directory, "arm64")).ok).toBe(false);
|
||||
expect(
|
||||
(
|
||||
await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
})
|
||||
).ok,
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -612,7 +681,10 @@ describe("iOS-style app bundle targets", () => {
|
|||
await writeFile(outside, thinMach(0xfeedfacf, 0x0100000c));
|
||||
await writeFile(join(app, "Info.plist"), plist("Escaping"));
|
||||
await symlink(outside, join(app, "Escaping"));
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
if (result.ok) throw new Error("Expected an escaping program file");
|
||||
expect(result.error.message).toContain("leaves the bundle root");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,13 +11,11 @@ describe("DOS COM admission", () => {
|
|||
const path = join(directory, "fixture.js");
|
||||
const bytes = Buffer.from("b83412c3", "hex");
|
||||
await writeFile(path, bytes);
|
||||
const target = await parseBinaryTarget(
|
||||
path,
|
||||
directory,
|
||||
"arm64",
|
||||
undefined,
|
||||
"dos-com",
|
||||
);
|
||||
const target = await parseBinaryTarget(path, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
formatHint: "dos-com",
|
||||
});
|
||||
expect(target).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
|
|
@ -38,7 +36,11 @@ describe("DOS COM admission", () => {
|
|||
const path = join(directory, "fixture.com");
|
||||
await writeFile(path, Buffer.alloc(length));
|
||||
expect(
|
||||
await parseBinaryTarget(path, directory, "x64", undefined, "dos-com"),
|
||||
await parseBinaryTarget(path, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "x64",
|
||||
formatHint: "dos-com",
|
||||
}),
|
||||
).toMatchObject({ ok: false, error: { _tag: "BinaryTargetError" } });
|
||||
});
|
||||
it("rejects contradictory target kind", async () => {
|
||||
|
|
@ -46,7 +48,12 @@ describe("DOS COM admission", () => {
|
|||
const path = join(directory, "fixture.com");
|
||||
await writeFile(path, Buffer.from("c3", "hex"));
|
||||
expect(
|
||||
await parseBinaryTarget(path, directory, "x64", "database", "dos-com"),
|
||||
await parseBinaryTarget(path, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "x64",
|
||||
targetKind: "database",
|
||||
formatHint: "dos-com",
|
||||
}),
|
||||
).toMatchObject({ ok: false, error: { _tag: "BinaryTargetError" } });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, it as test, vi } from "vitest";
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
import { access, readFile, truncate, writeFile } from "node:fs/promises";
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { AndroidAnalysisService } from "../../../src/application/android/AndroidAnalysisService.js";
|
||||
import { JadxProvider } from "../../../src/android/JadxProvider.js";
|
||||
|
|
@ -17,6 +17,30 @@ import { writeJadxJarInventory } from "../../fixtures/android/jadxJar.js";
|
|||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
|
||||
it("cancels Android admission while hashing the selected APK", async () => {
|
||||
const { service, apk, launches, provider } = await setup();
|
||||
await writeFile(apk, Buffer.from([0x50, 0x4b, 0x03, 0x04]));
|
||||
await truncate(apk, 1024 * 1024 * 1024);
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 100);
|
||||
try {
|
||||
const result = await service.execute(
|
||||
"inspect_android_package",
|
||||
{ path: apk },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisCancelledError" },
|
||||
});
|
||||
expect(launches).toHaveLength(0);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
await provider.close();
|
||||
await verifyCleanup(launches);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a copied JAR that differs from its admitted session identity", async () => {
|
||||
const { jar } = await setup();
|
||||
const admitted = await hashAndroidFile(jar);
|
||||
|
|
|
|||
Loading…
Reference in New Issue