fix: retain invalid runtime seeds for long-lived providers

This commit is contained in:
morluto committed 2026-11-11 20:16:24 +08:00
1 parent 64d1a57aef
commit ef754ce717
3 files changed
+221 -70

No files matched your search

+73
View File
@@ -14,6 +14,7 @@ import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSna
import {
runtimeAnalysisConfig,
runtimeAsarHeaderAdmission,
runtimeCaptureDecoderHeap,
} from "../config/runtimeAnalysisSettings.js";
import { parseConfig } from "../config/parseConfig.js";
@@ -391,3 +392,75 @@ it("keeps the Hopper startup deadline source through a runtime override", () =>
expect(live.hopperStartupTimeoutSource).toBe("runtime");
});
});
it("retains invalid ASAR seeds across long-lived views and override resets", async () => {
const environment = {
REA_ASAR_MAX_HEADER_BYTES: "invalid",
REA_ASAR_MAX_LISTING_BYTES: "invalid",
};
const store = new AnalysisSettings(environment);
const admission = runtimeAsarHeaderAdmission(environment, store.read);
expect(() => admission.maximumBytes).toThrow("REA_ASAR_MAX_HEADER_BYTES");
expect(() => admission.listing?.source).toThrow("REA_ASAR_MAX_LISTING_BYTES");
store.update({
changes: [
{ setting: "asar.max_header_bytes", action: "set", value: 8192 },
{ setting: "asar.max_listing_bytes", action: "set", value: 4096 },
],
});
let release!: () => void;
const barrier = new Promise<void>((resolve) => {
release = resolve;
});
const accepted = store.run(async () => {
await barrier;
expect(admission).toMatchObject({
maximumBytes: 8192,
source: "runtime",
listing: { maximumBytes: 4096, source: "runtime" },
});
});
store.update({
changes: [
{ setting: "asar.max_header_bytes", action: "reset" },
{ setting: "asar.max_listing_bytes", action: "reset" },
],
});
release();
await accepted;
for (const read of [() => admission.maximumBytes, () => admission.source])
expect(read).toThrow("REA_ASAR_MAX_HEADER_BYTES");
for (const read of [
() => admission.listing?.maximumBytes,
() => admission.listing?.source,
])
expect(read).toThrow("REA_ASAR_MAX_LISTING_BYTES");
});
it("retains an invalid capture heap seed through override and reset", () => {
const environment = { REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "invalid" };
const store = new AnalysisSettings(environment);
const heap = runtimeCaptureDecoderHeap(environment, store.read);
expect(() => heap.oldGenerationMiB).toThrow(
"REA_CAPTURE_DECODER_OLD_GENERATION_MIB",
);
store.update({
changes: [
{
setting: "capture.decoder_old_generation_mib",
action: "set",
value: 64,
},
],
});
expect(heap).toMatchObject({ oldGenerationMiB: 64, source: "runtime" });
store.update({
changes: [
{ setting: "capture.decoder_old_generation_mib", action: "reset" },
],
});
expect(() => heap.oldGenerationMiB).toThrow(
"REA_CAPTURE_DECODER_OLD_GENERATION_MIB",
);
expect(() => heap.source).toThrow("REA_CAPTURE_DECODER_OLD_GENERATION_MIB");
});
+58 -70
View File
@@ -11,17 +11,13 @@ import {
DEFAULT_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION,
DEFAULT_GHIDRA_VALUE_FLOW_MAX_EDGES,
} from "../domain/analysisResourceLimits.js";
import { parseAsarListingAdmission } from "./asarListingAdmission.js";
import {
parseAsarListingAdmission,
DEFAULT_ASAR_LISTING_ADMISSION,
} from "./asarListingAdmission.js";
import {
DEFAULT_ASAR_HEADER_ADMISSION,
parseAsarHeaderAdmission,
type AsarHeaderAdmission,
} from "./asarHeaderAdmission.js";
import {
resolveCaptureDecoderHeap,
parseCaptureDecoderHeap,
type CaptureDecoderHeap,
} from "./captureDecoderHeap.js";
import {
@@ -172,78 +168,70 @@ export const runtimeAsarHeaderAdmission = (
const parsed = parseAsarHeaderAdmission(
environment.REA_ASAR_MAX_HEADER_BYTES,
);
const headerOverride =
read?.().overrides["asar.max_header_bytes"] ??
currentAnalysisSettingOverride("asar.max_header_bytes");
if (!parsed.ok && headerOverride === undefined) throw parsed.error;
const listing = parseAsarListingAdmission(
environment.REA_ASAR_MAX_LISTING_BYTES,
);
const listingOverride =
read?.().overrides["asar.max_listing_bytes"] ??
currentAnalysisSettingOverride("asar.max_listing_bytes");
if (!listing.ok && listingOverride === undefined) throw listing.error;
const baseline = parsed.ok ? parsed.value : DEFAULT_ASAR_HEADER_ADMISSION;
const listingBaseline = listing.ok
? listing.value
: DEFAULT_ASAR_LISTING_ADMISSION;
return read === undefined
? {
maximumBytes: headerOverride ?? baseline.maximumBytes,
source: headerOverride === undefined ? baseline.source : "runtime",
listing: {
maximumBytes: listingOverride ?? listingBaseline.maximumBytes,
source:
listingOverride === undefined ? listingBaseline.source : "runtime",
},
}
: {
listing: {
get maximumBytes() {
return (
read().overrides["asar.max_listing_bytes"] ??
listingBaseline.maximumBytes
);
},
get source() {
return read().overrides["asar.max_listing_bytes"] === undefined
? listingBaseline.source
: "runtime";
},
},
get maximumBytes() {
return (
read().overrides["asar.max_header_bytes"] ?? baseline.maximumBytes
);
},
get source() {
return read().overrides["asar.max_header_bytes"] === undefined
? baseline.source
: "runtime";
},
};
const header = () => {
const override =
read?.().overrides["asar.max_header_bytes"] ??
currentAnalysisSettingOverride("asar.max_header_bytes");
if (override !== undefined)
return { maximumBytes: override, source: "runtime" as const };
if (!parsed.ok) throw parsed.error;
return parsed.value;
};
const expandedListing = () => {
const override =
read?.().overrides["asar.max_listing_bytes"] ??
currentAnalysisSettingOverride("asar.max_listing_bytes");
if (override !== undefined)
return { maximumBytes: override, source: "runtime" as const };
if (!listing.ok) throw listing.error;
return listing.value;
};
if (read === undefined) return { ...header(), listing: expandedListing() };
return {
get maximumBytes() {
return header().maximumBytes;
},
get source() {
return header().source;
},
listing: {
get maximumBytes() {
return expandedListing().maximumBytes;
},
get source() {
return expandedListing().source;
},
},
};
};
export const runtimeCaptureDecoderHeap = (
environment: Readonly<Record<string, string | undefined>>,
read?: AnalysisSettingsReader,
): CaptureDecoderHeap => {
const baseline = resolveCaptureDecoderHeap(environment);
return read === undefined
? baseline
: {
get oldGenerationMiB() {
return (
read().overrides["capture.decoder_old_generation_mib"] ??
baseline.oldGenerationMiB
);
},
get source() {
return read().overrides["capture.decoder_old_generation_mib"] ===
undefined
? baseline.source
: "runtime";
},
};
const parsed = parseCaptureDecoderHeap(
environment.REA_CAPTURE_DECODER_OLD_GENERATION_MIB,
);
const effective = () => {
const override =
read?.().overrides["capture.decoder_old_generation_mib"] ??
currentAnalysisSettingOverride("capture.decoder_old_generation_mib");
if (override !== undefined)
return { oldGenerationMiB: override, source: "runtime" as const };
if (!parsed.ok) throw parsed.error;
return parsed.value;
};
if (read === undefined) return effective();
return {
get oldGenerationMiB() {
return effective().oldGenerationMiB;
},
get source() {
return effective().source;
},
};
};
/**
* Resolve the accepted operation's allowance on each call. An invalid launch
@@ -428,3 +428,93 @@ it("lowers, restores and seeds the native matching allowance over MCP", async ()
await invalid.client.close();
}
}, 30000);
it("starts with invalid runtime seeds and recovers ASAR and capture work on the same connection", async () => {
const root = await createTestTempDirectory("rea-invalid-runtime-seeds-");
const archive = join(root, "small.asar");
const capture = join(root, "capture.har");
const payload = Buffer.from("answer");
await writeFile(
archive,
Buffer.concat([
asarHeaderBytes(
JSON.stringify({
files: { "answer.txt": { size: payload.length, offset: "0" } },
}),
),
payload,
]),
);
await writeFile(
capture,
JSON.stringify({
log: {
version: "1.2",
creator: { name: "test", version: "1" },
entries: [],
},
}),
);
const { client, call } = await connectLocalToolsMcp({
REA_ASAR_MAX_HEADER_BYTES: "invalid",
REA_ASAR_MAX_LISTING_BYTES: "invalid",
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "invalid",
});
try {
const changes = [
{ setting: "asar.max_header_bytes", action: "set", value: 8192 },
{ setting: "asar.max_listing_bytes", action: "set", value: 4096 },
{
setting: "capture.decoder_old_generation_mib",
action: "set",
value: 64,
},
];
expect(
(await call("update_analysis_settings", { changes })).isError,
).not.toBe(true);
expect((await call("open_binary", { path: archive })).isError).not.toBe(
true,
);
expect((await call("inspect_artifact", {})).isError).not.toBe(true);
const inspected = await call("inspect_web_network_capture", {
capture_path: capture,
format: "har",
});
expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true);
expect(
(
await call("update_analysis_settings", {
changes: changes.map(({ setting }) => ({ setting, action: "reset" })),
})
).isError,
).not.toBe(true);
const refusedArchive = await call("inspect_artifact", {});
expect(refusedArchive.isError).toBe(true);
expect(JSON.stringify(refusedArchive)).toContain(
"REA_ASAR_MAX_HEADER_BYTES",
);
const refused = await call("inspect_web_network_capture", {
capture_path: capture,
format: "har",
});
expect(refused.isError).toBe(true);
expect(JSON.stringify(refused)).toContain(
"REA_CAPTURE_DECODER_OLD_GENERATION_MIB",
);
expect(
(await call("update_analysis_settings", { changes })).isError,
).not.toBe(true);
expect((await call("inspect_artifact", {})).isError).not.toBe(true);
expect(
(
await call("inspect_web_network_capture", {
capture_path: capture,
format: "har",
})
).isError,
).not.toBe(true);
} finally {
await client.close();
}
}, 30000);