fix(javascript): trace wrapped Electron IPC paths in large bundles (#1874)

Co-authored-by: David <17119984+fengdwx@users.noreply.github.com>
This commit is contained in:
DavidandDavid authored and GitHub committed 2026-10-12 09:28:59 +08:00
1 parent d93fbda390
commit 87b9793c53
46 files changed
+3213 -355

No files matched your search

@@ -20,6 +20,7 @@ import {
export const JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES = 8 * 1024 * 1024;
const FRAME_BYTES = JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES;
const TRANSFER_BYTES = 512 * 1024 * 1024;
const MAX_TRANSFER_BYTES = 2 * 1024 * 1024 * 1024;
const BATCH_BYTES = 1024 * 1024;
const recordSchema = z.discriminatedUnion("kind", [
z.strictObject({
@@ -36,7 +37,7 @@ const recordSchema = z.discriminatedUnion("kind", [
/** Complete-file coordinates authenticated before adopting any worker facts. */
export const javaScriptAnalysisTransferDescriptorSchema = z.strictObject({
bytes: z.number().int().nonnegative().max(TRANSFER_BYTES),
bytes: z.number().int().nonnegative().max(MAX_TRANSFER_BYTES),
sha256: digestSchema,
records: z.number().int().nonnegative(),
module_bytes: z.number().int().nonnegative(),
@@ -52,11 +53,24 @@ export const javaScriptTransferRetentionBytes = (
records: number,
): number => bytes * 8 + records * 64;
const encodeTransferRecord = (input: unknown, previousBytes: number) => {
/** Larger streamed files require corresponding parent admission capacity. */
export const javaScriptTransferByteBudget = (
resultBudgetBytes: number,
): number =>
Math.min(
MAX_TRANSFER_BYTES,
Math.max(TRANSFER_BYTES, Math.floor(resultBudgetBytes / 8)),
);
const encodeTransferRecord = (
input: unknown,
previousBytes: number,
transferBytes = TRANSFER_BYTES,
) => {
const record = recordSchema.parse(input);
const line = `${JSON.stringify(record)}\n`;
const size = Buffer.byteLength(line);
if (size > FRAME_BYTES || previousBytes + size > TRANSFER_BYTES)
if (size > FRAME_BYTES || previousBytes + size > transferBytes)
throw new AnalysisResourceConstraintError(
"analyze_javascript_application",
"transport",
@@ -65,7 +79,7 @@ const encodeTransferRecord = (input: unknown, previousBytes: number) => {
: "Complete JavaScript file facts exceed the transfer byte budget",
{
transfer_record_bytes: FRAME_BYTES,
transfer_file_bytes: TRANSFER_BYTES,
transfer_file_bytes: transferBytes,
record_bytes: size,
transfer_bytes_at_least: previousBytes + size,
},
@@ -108,7 +122,16 @@ export const writeJavaScriptAnalysisTransfer = async (
path: string,
module: JavaScriptModuleSemanticIr,
projection: JavaScriptSemanticFileProjection,
transferBytes = TRANSFER_BYTES,
): Promise<Descriptor> => {
if (
!Number.isSafeInteger(transferBytes) ||
transferBytes < 1 ||
transferBytes > MAX_TRANSFER_BYTES
)
throw new RangeError(
"JavaScript transfer requires a valid complete-file byte budget",
);
const output = await open(path, "wx", 0o600);
const digest = createHash("sha256");
let bytes = 0;
@@ -129,7 +152,11 @@ export const writeJavaScriptAnalysisTransfer = async (
batchBytes = 0;
};
const append = async (input: unknown): Promise<void> => {
const { line, size, kind } = encodeTransferRecord(input, bytes);
const { line, size, kind } = encodeTransferRecord(
input,
bytes,
transferBytes,
);
bytes += size;
records += 1;
if (
@@ -1,7 +1,10 @@
import { z } from "zod";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { digestSchema } from "../../domain/digests.js";
import { JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS } from "../../domain/javascript/javascriptAnalysisResourceControls.js";
import {
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
JAVASCRIPT_SEMANTIC_NODE_MAXIMUM,
} from "../../domain/javascript/javascriptAnalysisResourceControls.js";
import { javaScriptAnalysisTransferDescriptorSchema } from "./JavaScriptAnalysisTransfer.js";
/** Independent worker execution budget; source length is not an admission rule. */
@@ -14,7 +17,7 @@ export const JAVASCRIPT_ANALYSIS_WORKER_DEFAULTS = {
/** Caller-selected artifact identity, separate from the decoded source-text digest. */
export const javaScriptWorkerRequestSchema = z.strictObject({
id: z.uuid(),
node_budget: z.number().int().min(0).max(20_000),
node_budget: z.number().int().min(0).max(JAVASCRIPT_SEMANTIC_NODE_MAXIMUM),
text_sha256: digestSchema,
file: z.strictObject({
path: z.string(),
@@ -23,13 +23,19 @@ const findingContext = {
module_runtime: z.string().optional(),
location: range,
};
const withRuntime = <Value extends { module_runtime?: string | undefined }>(
const withRuntime = <
Value extends {
module_runtime?: string | undefined;
resolution_basis?: "wrapper-substitution" | undefined;
},
>(
value: Value,
) => {
const { module_runtime, ...rest } = value;
const { module_runtime, resolution_basis, ...rest } = value;
return {
...rest,
...(module_runtime === undefined ? {} : { module_runtime }),
...(resolution_basis === undefined ? {} : { resolution_basis }),
};
};
const staticValue = z.discriminatedUnion("status", [
@@ -62,12 +68,14 @@ const ipcChannel = <Shape extends z.ZodRawShape>(shape: Shape) =>
z.union([
z.strictObject({
...findingContext,
resolution_basis: z.literal("wrapper-substitution").optional(),
...shape,
channel: z.string(),
channel_expression: z.null(),
}),
z.strictObject({
...findingContext,
resolution_basis: z.literal("wrapper-substitution").optional(),
...shape,
channel: z.null(),
channel_expression: z.string(),
@@ -113,10 +121,11 @@ const ipc = z
}),
])
.transform((value) => {
const { module_runtime, ...rest } = value;
const { module_runtime, resolution_basis, ...rest } = value;
return {
...rest,
...(module_runtime === undefined ? {} : { module_runtime }),
...(resolution_basis === undefined ? {} : { resolution_basis }),
};
});
const windowShape = {
@@ -128,6 +137,7 @@ const windowShape = {
),
};
const bridgeShape = {
resolution_basis: z.literal("wrapper-substitution").optional(),
...findingContext,
world: z.enum(["main", "isolated"]),
world_id: staticValue.nullable(),
@@ -136,99 +146,134 @@ const bridgeShape = {
unknown_members: count,
};
const utilityShape = { ...findingContext, service_name: z.string().nullable() };
const electron = z.strictObject({
browser_windows: z.array(
z.union([
z
.strictObject({
...windowShape,
preload_path: z.string(),
preload_resolution_context: pathContext,
})
.transform(withRuntime),
z
.strictObject({
...windowShape,
preload_path: z.null(),
preload_resolution_context: z.null(),
})
.transform(withRuntime),
]),
),
context_bridge_apis: z.array(
z.union([
z
.strictObject({
...bridgeShape,
api_key: z.string(),
api_key_expression: z.null(),
})
.transform(withRuntime),
z
.strictObject({
...bridgeShape,
api_key: z.null(),
api_key_expression: z.string(),
})
.transform(withRuntime),
z
.strictObject({
...bridgeShape,
api_key: z.null(),
api_key_expression: z.null(),
})
.transform(withRuntime),
]),
),
ipc: z.array(ipc),
sender_validations: z.array(
z.strictObject({
...findingContext,
subject: z.enum([
"sender-url",
"sender-origin",
"sender-frame",
"sender-id",
"frame-id",
"process-id",
const electron = z
.strictObject({
call_paths: z
.array(
z.strictObject({
entry_kind: z.enum(["bridge-member", "ipc-handler"]),
entry: z.string(),
channel: z.string().nullable(),
terminal: z.string(),
resolution: z.literal("candidate"),
steps: z.array(
z.strictObject({
kind: z.enum([
"exposure",
"factory",
"function",
"call",
"ipc",
"terminal",
]),
label: z.string(),
location: range,
module_key: z.string().nullable(),
module_runtime: z.string().nullable(),
}),
),
conditions: z.array(
z.strictObject({ expression: z.string(), location: range }),
),
}),
)
.optional(),
browser_windows: z.array(
z.union([
z
.strictObject({
...windowShape,
preload_path: z.string(),
preload_resolution_context: pathContext,
})
.transform(withRuntime),
z
.strictObject({
...windowShape,
preload_path: z.null(),
preload_resolution_context: z.null(),
})
.transform(withRuntime),
]),
mechanism: z.string(),
expected: staticValue,
enforcement: z.literal("unknown"),
}),
),
utility_processes: z.array(
z.union([
z
.strictObject({
...utilityShape,
module_path: z.string(),
module_resolution_context: pathContext,
module_expression: z.null(),
})
.transform(withRuntime),
z
.strictObject({
...utilityShape,
module_path: z.null(),
module_resolution_context: z.null(),
module_expression: z.string(),
})
.transform(withRuntime),
]),
),
native_addon_bindings: z.array(
z.strictObject({
...findingContext,
specifier: z.string(),
binding_kind: z.enum(["import", "require", "re-export"]),
module_kind: z.enum(["import", "require"]),
members: strings,
namespace_access: z.boolean().optional(),
dynamic_member_access: z.boolean().optional(),
}),
),
});
),
context_bridge_apis: z.array(
z.union([
z
.strictObject({
...bridgeShape,
api_key: z.string(),
api_key_expression: z.null(),
})
.transform(withRuntime),
z
.strictObject({
...bridgeShape,
api_key: z.null(),
api_key_expression: z.string(),
})
.transform(withRuntime),
z
.strictObject({
...bridgeShape,
api_key: z.null(),
api_key_expression: z.null(),
})
.transform(withRuntime),
]),
),
ipc: z.array(ipc),
sender_validations: z.array(
z.strictObject({
...findingContext,
subject: z.enum([
"sender-url",
"sender-origin",
"sender-frame",
"sender-id",
"frame-id",
"process-id",
]),
mechanism: z.string(),
expected: staticValue,
enforcement: z.literal("unknown"),
}),
),
utility_processes: z.array(
z.union([
z
.strictObject({
...utilityShape,
module_path: z.string(),
module_resolution_context: pathContext,
module_expression: z.null(),
})
.transform(withRuntime),
z
.strictObject({
...utilityShape,
module_path: z.null(),
module_resolution_context: z.null(),
module_expression: z.string(),
})
.transform(withRuntime),
]),
),
native_addon_bindings: z.array(
z.strictObject({
...findingContext,
specifier: z.string(),
binding_kind: z.enum(["import", "require", "re-export"]),
module_kind: z.enum(["import", "require"]),
members: strings,
namespace_access: z.boolean().optional(),
dynamic_member_access: z.boolean().optional(),
}),
),
})
.transform(({ call_paths, ...rest }) => ({
...rest,
...(call_paths === undefined ? {} : { call_paths }),
}));
const referenceShape = {
...findingContext,
kind: z.enum([
@@ -9,7 +9,10 @@ import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptA
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptModuleSemanticIr } from "../../domain/javascript/javascriptModuleSemanticIr.js";
import type { JavaScriptSemanticFileProjection } from "../../domain/javascript/javascriptSemanticFileProjection.js";
import { writeJavaScriptAnalysisTransfer } from "./JavaScriptAnalysisTransfer.js";
import {
javaScriptTransferByteBudget,
writeJavaScriptAnalysisTransfer,
} from "./JavaScriptAnalysisTransfer.js";
import { javaScriptWorkerStaticAnalysisSchema } from "./JavaScriptAnalysisWorkerSchemas.js";
import {
JAVASCRIPT_ANALYSIS_WORKER_DEFAULTS as DEFAULTS,
@@ -95,6 +98,7 @@ const analyzeJob = async (
requests: AsyncIterator<string>,
): Promise<void> => {
try {
let resultBudgetBytes = 0;
const source = await readFile(join(root, `${request.id}.source`), "utf8");
if (
createHash("sha256").update(source).digest("hex") !== request.text_sha256
@@ -148,7 +152,8 @@ const analyzeJob = async (
throw new TypeError(
"JavaScript worker admission names a different source job",
);
return admission.semantic_result_budget_bytes;
resultBudgetBytes = admission.semantic_result_budget_bytes;
return resultBudgetBytes;
},
},
);
@@ -160,6 +165,7 @@ const analyzeJob = async (
join(root, `${request.id}.records`),
result.module,
result.projection,
javaScriptTransferByteBudget(resultBudgetBytes),
);
await send({ kind: "complete", id: request.id, descriptor });
} catch (cause: unknown) {