fix(windows): preserve transient runtime admission failures (#2467)

Fixes #2257. Thanks to @chqcjq for the native self-test retry diagnosis in #2266.

Preserve retry disposition through analysis workflows. Native warm admission requires exact sharing/lock codes and affirmative pre-allocation evidence; cleanup ownership takes precedence. No automatic retry or new analysis cache.

Verified with real Windows 11 CLI, same-connection SDK MCP normal/conflict/recovery, the Windows native verifier, 99 focused tests and static/docs gates. Official npm 11.16.0 launcher sources were the application input. No IDA or Ghidra engine was used for this change. Win32 5 was not reproduced; no resolution claim for #1701/#2196. Performance measurements and limits are recorded in experiments/analysis-efficiency/windows-runtime-retry.md.
This commit is contained in:
Ysopwnable 2026-10-13 11:42:54 +08:00 committed by GitHub
parent 23de4f77c7
commit 172407787f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 491 additions and 62 deletions

View File

@ -0,0 +1,107 @@
# Windows runtime admission retry verification
Scope: [#2257](https://github.com/morluto/rea/issues/2257). Related initial
diagnosis: [#2266](https://github.com/morluto/rea/pull/2266).
## Revisions and real input
The baseline is `4f4f6cd7b5d7a400b90772a138d7b9f942c2a556`.
The candidate was developed from
`4773c152bc5f299fa682575bb8d1efea7587c7bc`; relevant runtime, native and
dependency-lock sources are identical between those two revisions.
These measurements precede integration of subsequent main changes.
The host ran Windows 11 10.0.22631.0, official Windows x64 Node 24.18.0,
and locked production dependencies. The real application input was the official
npm 11.16.0 launcher distribution's `bin` directory. REA inspected that source;
it did not execute the application as an analysis target. Runtime copies,
raw replies and test executables remain outside the checkout. No IDA or Ghidra
engine was involved in this native-runtime verification.
## Correctness and recovery
A test-owned directory handle requesting `FILE_ADD_FILE` produced an actual
kernel sharing violation (Win32 32) during native runtime-parent admission.
Releasing that handle restored admission. Before the fix, public CLI analysis
reported a permanent `unsupported_provider` failure with `retryable: false`.
The corrected CLI preserves `capability_unavailable` and the reason, with
`category: unavailable` and `retryable: true`.
Normal CLI analysis and post-conflict recovery produced the same canonical
normalized result before and after the correction: 535,733 compact JSON bytes,
SHA-256 `a24d3c2d9897cccc5ba70e9ee4dce03da88f033a939fe87457136fc5b3a3e2db`.
The complete successful CLI reply remained 537,124 bytes.
A single real SDK 2.3.1 stdio MCP connection first completed analysis, then
encountered the same kernel conflict after native authority caching, then
recovered successfully when the handle was released. Its final receipt passed
and found no residual runtime directories. The native producer supplies
`runtimeAllocation: "not-started"` only when parent admission failed before
directory creation. The adapter requires that fact and exact numeric codes
32/33; permission failures, opaque error text and unknown allocation state
remain nonretryable. Incomplete cleanup also prevents retry.
The native verifier additionally reproduces a real sharing conflict using an
owned runtime's DELETE lease, verifies the original error and allocation fact,
and confirms no child directory was allocated. This case runs unconditionally
in the Windows native verifier, without the optional process-boundary fixture.
The full verifier passed with 20 positive controls. Its optional independently
built process-boundary fixture was not supplied in this run.
The related focused suite passed 99 tests across seven files, including worker
ownership, cancellation, cleanup, error projection, loader behavior and adapter
boundaries. Source-map and WABT acquisition-failure tests use injected failures;
they establish disposition propagation, not real engine execution.
Thirty baseline native self-tests passed with no residual directories.
Win32 5 from #1701/#2196 was not reproduced on this host; these results do not
establish its cause or resolution.
## Delivery and resource measurements
One final same-connection MCP trial used one CPU per Node process, BelowNormal
priority, a 512 MiB client heap setting and a 768 MiB server heap setting.
Heap settings do not bound total process or family memory.
| Stage | Observed wall time | Serialized reply bytes |
| ---------------------------------- | -----------------: | ---------------------: |
| Connect | 11,351 ms | Not recorded |
| Complete tool discovery, 187 tools | 744 ms | 2,498,686 |
| Normal analysis | 12,887 ms | 1,126,797 |
| Sharing failure | 265 ms | 878 |
| Recovery on the same connection | 5,578 ms | 1,126,797 |
The earlier warm failure reply was 1,218 bytes. Successful reply sizes and the
catalogue digest were unchanged within the tested source comparison:
`7bdadddc909f4b2b093f0b3da9f79fafdac879c9d3c147938b8fbce1b52cd726`.
These are delivery bytes, not model tokens. Model-visible discovery handling,
actual model token usage and total model task cost were not measured.
A targeted process-generation sampler collected 343 observations over
35,630 ms. Its aggregate observed CPU time was a **lower bound** of 26,766 ms:
process exits can hide the final CPU tail. Its peak sampled working-set sum was
727,244,800 bytes (about 694 MiB). This is a sampled sum, can double-count shared
pages and is not an exact physical-memory peak. Only the test-owned Node and
directory-holder processes were included; the observer itself was excluded.
This is one controlled functional trial, with no paired CPU/memory baseline.
Cold/warm filesystem caches and host noise were not normalized. No speedup,
CPU reduction, memory reduction or model-token savings are claimed. The change
adds no analysis retry loop or cache and performs no additional engine calls.
Existing successful native authority caching remains intact; failed readiness
is retried by a later caller attempt. Retry becomes useful advice only after
the transient condition clears and cleanup permits another attempt.
## Integration replay
After rebasing onto main `a2d8b94dd`, runtime candidate
`2c7a09eaf3ffe26667b40081f1cf0fd2ce44a146` again passed the 99 related tests,
static and documentation gates, real Windows CLI and same-connection MCP
normal/conflict/recovery sequence. Both workflows found no residual directories;
the CLI normalized-result digest above still matched. The MCP success and error
reply sizes remained 1,126,797 and 878 bytes respectively. Upstream Android/SAZ
contract changes increased complete discovery to 2,517,904 bytes, digest
`d14588eab7404ad129907eb34a45a0db0d43233c4de829f76babd59723bed199`.
This later catalogue is not evidence of a delivery-size change caused by the
runtime-admission fix. Resource sampling above belongs to the earlier pinned
candidate; aggregate resources were not resampled for this integration replay.

View File

@ -85,6 +85,17 @@ Windows failures retain their constraint, requested coordinate, Win32 code,
and system message. Unsupported filesystems and reparse paths are distinct
from OS access denial and missing packaged controls.
A failed runtime-parent admission additionally reports
`runtimeAllocation: "not-started"`: no runtime directory was created. The
Windows adapter uses that fact with numeric sharing/lock codes 32/33 to retain
a retryable private-runtime failure, including after successful native
authority caching. Missing allocation evidence, other codes, and opaque error
text do not establish a safe retry. Analysis errors propagate the producer's
retry disposition through JavaScript, source-map and WABT workflows; transient
failures report `category: unavailable` and retain the original reason.
Incomplete cleanup blocks an operation retry until its owned resources settle.
REA adds no automatic retry loop or new analysis cache for this behavior.
Ordinary drive-absolute paths accept backslashes, forward slashes, or mixed
separators. File identity retains the original requested spelling. Separator
translation does not resolve dot components, repeated separators, or extend

View File

@ -87,6 +87,8 @@ napi_value failureValue(napi_env env, const Failure& failure) {
set(env, error, "requestedPath", string(env, failure.path));
set(env, error, "win32Code", number(env, failure.win32));
set(env, error, "win32Message", string(env, detail));
if (failure.runtimeDirectoryUnallocated)
set(env, error, "runtimeAllocation", string(env, std::string("not-started")));
return error;
}

View File

@ -33,6 +33,8 @@ struct Failure : std::runtime_error {
DWORD win32;
std::wstring path;
std::string constraint;
// Affirmative proof for a failed runtime-parent admission; omission is unknown.
bool runtimeDirectoryUnallocated = false;
Failure(std::string constraint, std::wstring path = L"", DWORD code = GetLastError())
: std::runtime_error(constraint), win32(code), path(std::move(path)),
constraint(std::move(constraint)) {}

View File

@ -212,7 +212,12 @@ static void verifyPrivate(HANDLE handle, const std::wstring& path, bool root) {
}
std::unique_ptr<Runtime> createRuntime(const std::wstring& parentPath, const std::wstring& prefix) {
auto parent = openFile(parentPath, 0, true);
std::unique_ptr<File> parent;
try { parent = openFile(parentPath, 0, true); }
catch (Failure& failure) {
failure.runtimeDirectoryUnallocated = true;
throw;
}
require(!prefix.empty() && prefix.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos,
"Runtime prefix must be one filename component", parentPath, ERROR_INVALID_PARAMETER);
std::array<unsigned char, 16> random;

View File

@ -11,6 +11,7 @@ import {
mkdtemp,
readFile,
rename,
readdir,
rm,
symlink,
writeFile,
@ -37,6 +38,14 @@ const { WindowsPrivateRuntime, windowsPrivateRuntime } = await import(
const { ghidraSessionRoot } = await import(
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionRoot.js"))
);
const { PrivateRuntimeRoot } = await import(
pathToFileURL(join(packageRoot, "dist/process/PrivateRuntimeRoot.js"))
);
const { PrivateRuntimeRootUnavailableError } = await import(
pathToFileURL(
join(packageRoot, "dist/process/PrivateRuntimeRootUnavailableError.js"),
)
);
const workspace = await mkdtemp(join(tmpdir(), "rea-native-conformance-"));
const environment = Object.entries(process.env)
.filter(([, value]) => value !== undefined)
@ -140,6 +149,30 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
])
assert.throws(() => native.call("open", [rejected]));
report.controls.ordinaryDriveSeparatorsAndRequestedIdentity = true;
// An owned runtime's DELETE lease conflicts with a fresh parent admission.
// This produces a genuine sharing failure after successful authority caching,
// without another executable, and proves that no child runtime was created.
const sharingParent = WindowsPrivateRuntime.create(
workspace,
"rea-sharing-parent-",
);
try {
await assert.rejects(
PrivateRuntimeRoot.create({ parent: sharingParent.observation.path }),
(failure) => {
assert.ok(failure instanceof PrivateRuntimeRootUnavailableError);
assert.equal(failure.retryable, true);
assert.ok(failure.cause instanceof Error);
assert.equal(failure.cause.win32Code, 32);
assert.equal(failure.cause.runtimeAllocation, "not-started");
return true;
},
);
assert.deepEqual(await readdir(sharingParent.observation.path), []);
report.controls.transientRuntimeAdmissionBeforeAllocation = true;
} finally {
await sharingParent.close();
}
const driveRoot = parse(workspace).root;
const runtimeParent = ghidraSessionRoot({
base: join(driveRoot, ".Tmp", "scratch"),

View File

@ -100,18 +100,24 @@ export class AnalysisOutputError extends AnalysisError {
export class AnalysisCapabilityUnavailableError extends AnalysisError {
readonly _tag = "AnalysisCapabilityUnavailableError";
override readonly userMessage: string | undefined;
/** Incomplete cleanup must settle before the operation can be retried. */
readonly retryable: boolean;
constructor(
readonly providerId: string,
readonly operation: string,
readonly reason: string,
options?: AnalysisErrorOptions & { readonly userMessage?: string },
options?: AnalysisErrorOptions & {
readonly userMessage?: string;
readonly retryable?: boolean;
},
) {
super(
`Provider ${providerId} cannot execute ${operation}: ${reason}`,
options,
);
this.userMessage = options?.userMessage;
this.retryable = options?.retryable === true && !this.cleanupIncomplete;
}
}

View File

@ -1,6 +1,7 @@
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisUnsupportedTargetError,
AnalysisResourceConstraintError,
@ -33,6 +34,8 @@ import type { AnalysisErrorProjection } from "./analysisErrorProjection.js";
export const analysisErrorRemediationAction = (
error: AnalysisError,
): string => {
if (error instanceof AnalysisCapabilityUnavailableError && error.retryable)
return "Review details.reason, then retry the same analysis after the condition clears.";
if (error instanceof EvidenceFileError && error.published !== undefined)
return "Use the published file at details.published_path and resolve the reported staging cleanup failure.";
if (error instanceof AnalysisSnapshotMismatchError)
@ -112,6 +115,8 @@ export const analysisErrorRemediationAction = (
export const analysisErrorCategory = (
error: AnalysisError,
): AnalysisErrorProjection["category"] => {
if (error instanceof AnalysisCapabilityUnavailableError && error.retryable)
return "unavailable";
if (
error instanceof BinaryTargetError &&
error.constraint === "load_selection_rejected"
@ -220,6 +225,8 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
const hopperMessage = hopperErrorUserMessage(error);
if (hopperMessage !== undefined) return hopperMessage;
if (error.userMessage !== undefined) return error.userMessage;
if (error instanceof AnalysisCapabilityUnavailableError && error.retryable)
return "Required runtime controls are unavailable for this attempt.";
const standardMessage = standardErrorMessage(error._tag);
if (standardMessage !== undefined) return standardMessage;
if (error instanceof ArtifactOperationError) return artifactMessage(error);

View File

@ -25,6 +25,91 @@ import { ProviderSelectionError } from "./providerSelectionError.js";
import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js";
import { nativeCallPartialObservationSchema } from "./native/nativeCallPartialObservation.js";
describe("capability failure retry disposition", () => {
it.each([
{ options: {}, retryable: false, category: "unsupported_provider" },
{
options: { retryable: false },
retryable: false,
category: "unsupported_provider",
},
{ options: { retryable: true }, retryable: true, category: "unavailable" },
] as const)(
"preserves the producer's $retryable disposition",
({ options, retryable, category }) => {
const reason = "Native self-test failed with a sharing violation";
const projected = projectAnalysisError(
new AnalysisCapabilityUnavailableError(
"fixture-provider",
"inspect",
reason,
options,
),
);
expect(projected).toMatchObject({
code: "capability_unavailable",
category,
retryable,
details: { reason },
});
if (retryable) {
expect(projected.message).toBe(
"Required runtime controls are unavailable for this attempt.",
);
expect(projected.remediation.action).toContain(
"retry the same analysis",
);
}
expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe(
true,
);
},
);
it("preserves a caller-selected message for a retryable failure", () => {
expect(
projectAnalysisError(
new AnalysisCapabilityUnavailableError(
"fixture-provider",
"inspect",
"temporary failure",
{
retryable: true,
userMessage: "Selected runtime diagnostic",
},
),
).message,
).toBe("Selected runtime diagnostic");
});
it("retains cleanup ownership before allowing a transient operation to retry", () => {
expect(
projectAnalysisError(
new AnalysisCapabilityUnavailableError(
"fixture-provider",
"inspect",
"temporary failure",
{
retryable: true,
cleanup: {
reason: "owned handle is still live",
resources: ["native-owner"],
},
},
),
),
).toMatchObject({
code: "cleanup_incomplete",
retryable: false,
details: {
cleanup: "incomplete",
resources: ["native-owner"],
execution_failure: "capability_unavailable",
},
});
});
});
it("projects a published file receipt alongside retained cleanup failure", () => {
const projected = projectAnalysisError(
new EvidenceFileError("write", "io", {

View File

@ -83,6 +83,8 @@ export const projectAnalysisError = (
message: analysisErrorUserMessage(error),
retryable:
RETRYABLE_CODES.has(code) ||
(error instanceof AnalysisCapabilityUnavailableError &&
error.retryable) ||
(code === "resource_constraint" &&
error instanceof AnalysisResourceConstraintError &&
error.resource === "capacity"),

View File

@ -27,10 +27,8 @@ import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { ok, err, type Result } from "../../domain/result.js";
import { javaScriptRuntime } from "../../domain/runtimeVersion.js";
import {
PrivateRuntimeRoot,
PrivateRuntimeRootUnavailableError,
} from "../../process/PrivateRuntimeRoot.js";
import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js";
import { PrivateRuntimeRootUnavailableError } from "../../process/PrivateRuntimeRootUnavailableError.js";
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
import {
ProviderProcessSupervisor,
@ -735,7 +733,7 @@ export class JavaScriptAnalysisWorker {
PROVIDER,
OPERATION,
cause.message,
{ cause },
{ cause, retryable: cause.retryable },
)
: new ProviderAdapterError(PROVIDER, OPERATION, {
cause,

View File

@ -16,10 +16,8 @@ import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { err, ok, type Result } from "../../domain/result.js";
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
import {
PrivateRuntimeRoot,
PrivateRuntimeRootUnavailableError,
} from "../../process/PrivateRuntimeRoot.js";
import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js";
import { PrivateRuntimeRootUnavailableError } from "../../process/PrivateRuntimeRootUnavailableError.js";
import { ProviderStartupDeadline } from "../../process/ProviderDeadline.js";
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
import { cleanupOwnedProcessGroup } from "../../process/ProcessOwnership.js";
@ -261,7 +259,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
PROVIDER,
OPERATION,
cause.message,
{ cause },
{ cause, retryable: cause.retryable },
)
: new ProviderAdapterError(PROVIDER, OPERATION, {
cause,

View File

@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { WindowsPrivateRuntime } from "../windows/WindowsPrivateRuntime.js";
import { PrivateRuntimeRootUnavailableError } from "./PrivateRuntimeRootUnavailableError.js";
import {
windowsNativeCapabilities,
@ -34,16 +35,6 @@ export const privateRuntimeRootCapability = (
? windowsNativeCapabilities(platform).private_runtime_dacl
: { available: true, reason: null, proof: "posix-mkdtemp-owner-only" };
/** Expected failure when a runtime root's privacy boundary is unavailable. */
export class PrivateRuntimeRootUnavailableError extends Error {
readonly code = "private-runtime-root-authority-unavailable";
constructor(message: string) {
super(message);
this.name = "PrivateRuntimeRootUnavailableError";
}
}
/**
* Owns a private temporary runtime root and removes it idempotently.
*
@ -66,7 +57,9 @@ export class PrivateRuntimeRoot {
const platform = options.platform ?? process.platform;
const capability = privateRuntimeRootCapability(platform);
if (!capability.available)
throw new PrivateRuntimeRootUnavailableError(capability.reason);
throw new PrivateRuntimeRootUnavailableError(capability.reason, {
retryable: capability.retryable === true,
});
if (platform === "win32") {
const runtime = WindowsPrivateRuntime.create(
options.parent ?? tmpdir(),

View File

@ -0,0 +1,14 @@
/** Expected failure when a runtime root's privacy boundary is unavailable. */
export class PrivateRuntimeRootUnavailableError extends Error {
readonly code = "private-runtime-root-authority-unavailable";
readonly retryable: boolean;
constructor(
message: string,
options?: ErrorOptions & { readonly retryable?: boolean },
) {
super(message, options);
this.name = "PrivateRuntimeRootUnavailableError";
this.retryable = options?.retryable ?? false;
}
}

View File

@ -11,6 +11,7 @@ export type WindowsNativeCapability =
readonly available: false;
readonly reason: string;
readonly proof: "not-proven" | "not-applicable";
readonly retryable?: true;
};
/** Security controls required before REA may claim Windows isolation. */
@ -52,6 +53,7 @@ export const windowsNativeCapabilities = (
available: false,
reason: loaded.reason,
proof: "not-proven",
...(loaded.retryable === true ? { retryable: true as const } : {}),
} as const);
return {
job_object_process_ownership: capability,

View File

@ -3,12 +3,11 @@ import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { WabtArtifactProvider } from "./WabtArtifactProvider.js";
import {
PrivateRuntimeRoot,
PrivateRuntimeRootUnavailableError,
} from "../process/PrivateRuntimeRoot.js";
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
import { PrivateRuntimeRootUnavailableError } from "../process/PrivateRuntimeRootUnavailableError.js";
import { spawnOwnedProviderProcess } from "../process/ProviderProcess.js";
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
const failureTags: Readonly<Record<string, string>> = {
invalid: "AnalysisInputError",
@ -40,40 +39,51 @@ it("returns actionable unavailable without creating a workspace when tools are a
});
});
it("reports an unavailable private runtime root without a misleading WABT remediation", async () => {
const toolRoot = await createTestTempDirectory("rea-wabt-tool-root-");
for (const tool of ["wasm-validate", "wasm-objdump", "wasm2wat"])
await writeFile(
join(toolRoot, tool + (process.platform === "win32" ? ".exe" : "")),
"source-owned executable seam",
{ mode: 0o700 },
);
const provider = new WabtArtifactProvider(
{ REA_WABT_BIN_DIRECTORY: toolRoot },
undefined,
() => {
throw new PrivateRuntimeRootUnavailableError(
"REA Windows native controls are unavailable: probe failure",
it.each([false, true])(
"reports an unavailable private runtime root with retryable=%s without a misleading WABT remediation",
async (retryable) => {
const toolRoot = await createTestTempDirectory("rea-wabt-tool-root-");
for (const tool of ["wasm-validate", "wasm-objdump", "wasm2wat"])
await writeFile(
join(toolRoot, tool + (process.platform === "win32" ? ".exe" : "")),
"source-owned executable seam",
{ mode: 0o700 },
);
},
);
expect(
await provider.inspect({
const provider = new WabtArtifactProvider(
{ REA_WABT_BIN_DIRECTORY: toolRoot },
undefined,
() => {
throw new PrivateRuntimeRootUnavailableError(
"REA Windows native controls are unavailable: probe failure",
{ retryable },
);
},
);
const result = await provider.inspect({
path: "/module.wasm",
glue_paths: [],
candidate_paths: [],
}),
).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCapabilityUnavailableError",
userMessage: undefined,
message: expect.stringContaining(
"REA Windows native controls are unavailable",
),
},
});
});
});
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCapabilityUnavailableError",
retryable,
userMessage: undefined,
message: expect.stringContaining(
"REA Windows native controls are unavailable",
),
},
});
if (result.ok)
throw new Error("Unavailable runtime must refuse inspection");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "capability_unavailable",
retryable,
category: retryable ? "unavailable" : "unsupported_provider",
});
},
);
it.each([
"success",
"invalid",

View File

@ -11,7 +11,7 @@ import {
} from "../domain/analysisErrorCore.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
import { PrivateRuntimeRootUnavailableError } from "../process/PrivateRuntimeRoot.js";
import { PrivateRuntimeRootUnavailableError } from "../process/PrivateRuntimeRootUnavailableError.js";
import { WABT_LIMITS, WABT_PROVIDER_IDENTITY } from "./WabtRelease.js";
/** Preserve lifecycle failures and diagnostic bytes instead of returning partial success. */
export const wabtFailure = (
@ -76,7 +76,7 @@ export const wabtFailure = (
WABT_PROVIDER_IDENTITY.id,
"inspect_wasm_artifact",
reason,
{ cause },
{ cause, retryable: cause.retryable },
);
if (phase === "configuration")
return new AnalysisCapabilityUnavailableError(

View File

@ -1,6 +1,7 @@
import { win32 } from "node:path";
import { z } from "zod";
import { PrivateRuntimeRootUnavailableError } from "../process/PrivateRuntimeRootUnavailableError.js";
import {
requireWindowsNativeAuthority,
@ -23,6 +24,13 @@ const fileSchema = windowsFileIdentitySchema.extend({
handle: windowsHandleSchema,
});
const runtimes = new Map<string, WindowsPrivateRuntime>();
// Retry only a proven pre-allocation sharing/lock conflict, never opaque text
// or an operation that may already own an unpublished runtime directory.
const transientRuntimeAdmissionSchema = z.object({
code: z.literal("ERR_REA_WINDOWS_NATIVE"),
win32Code: z.union([z.literal(32), z.literal(33)]),
runtimeAllocation: z.literal("not-started"),
});
/** Retain private DACL and path handles until native recursive cleanup settles. */
export class WindowsPrivateRuntime {
@ -40,9 +48,21 @@ export class WindowsPrivateRuntime {
/** Create a new directory with its private DACL installed at creation. */
static create(parent: string, prefix: string): WindowsPrivateRuntime {
const authority = requireWindowsNativeAuthority();
const observation = runtimeSchema.parse(
authority.call("runtime_create", [parent, prefix]),
);
let observed: unknown;
try {
observed = authority.call("runtime_create", [parent, prefix]);
} catch (cause: unknown) {
if (
cause instanceof Error &&
transientRuntimeAdmissionSchema.safeParse(cause).success
)
throw new PrivateRuntimeRootUnavailableError(cause.message, {
cause,
retryable: true,
});
throw cause;
}
const observation = runtimeSchema.parse(observed);
const runtime = new WindowsPrivateRuntime(observation, authority);
runtimes.set(windowsRuntimeIdentityKey(observation), runtime);
return runtime;

View File

@ -0,0 +1,35 @@
import { expect, it } from "vitest";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { runSourceMapCommand } from "../../../src/javascript/sourceMaps/SourceMapCommand.js";
import { PrivateRuntimeRootUnavailableError } from "../../../src/process/PrivateRuntimeRootUnavailableError.js";
it.each([false, true])(
"preserves private-runtime retryable=%s through the source-map operation",
async (retryable) => {
const reason = "Native runtime self-test failed with a sharing violation";
let launches = 0;
const result = await runSourceMapCommand("{}", "/selected.map", undefined, {
prepareOwnership: async () => {},
createRuntime: async () => {
throw new PrivateRuntimeRootUnavailableError(reason, { retryable });
},
launcher: async () => {
launches += 1;
throw new Error("A refused runtime must not launch a codec");
},
});
expect(launches).toBe(0);
if (result.ok) throw new Error("Unavailable runtime must refuse decoding");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "capability_unavailable",
category: retryable ? "unavailable" : "unsupported_provider",
retryable,
details: {
provider_id: "source-map-decoder",
operation: "trace_web_source_location",
reason,
},
});
expect(result.error.cleanupIncomplete).toBe(false);
},
);

View File

@ -0,0 +1,99 @@
import { beforeEach, expect, it, vi } from "vitest";
import { WindowsPrivateRuntime } from "../../src/windows/WindowsPrivateRuntime.js";
import { PrivateRuntimeRootUnavailableError } from "../../src/process/PrivateRuntimeRootUnavailableError.js";
const { nativeCall } = vi.hoisted(() => ({ nativeCall: vi.fn() }));
vi.mock("../../src/windows/WindowsNativeLoader.js", async (original) => ({
...(await original<
typeof import("../../src/windows/WindowsNativeLoader.js")
>()),
requireWindowsNativeAuthority: () => ({ call: nativeCall }),
}));
beforeEach(() => {
nativeCall.mockReset();
});
it.each([
{
label: "sharing conflict before allocation",
win32Code: 32,
runtimeAllocation: "not-started",
retryable: true,
},
{
label: "lock conflict before allocation",
win32Code: 33,
runtimeAllocation: "not-started",
retryable: true,
},
{ label: "unknown allocation", win32Code: 32, retryable: false },
{
label: "possible allocation",
win32Code: 32,
runtimeAllocation: "started",
retryable: false,
},
{
label: "permission failure",
win32Code: 5,
runtimeAllocation: "not-started",
retryable: false,
},
{
label: "string code",
win32Code: "32",
runtimeAllocation: "not-started",
retryable: false,
},
{
label: "inconsistent error code",
win32Code: 32,
code: "EACCES",
runtimeAllocation: "not-started",
retryable: false,
},
])(
"retains $label without guessing runtime ownership",
({ retryable, label: _label, ...facts }) => {
const cause = Object.assign(new Error("Selected native diagnostic"), {
code: "ERR_REA_WINDOWS_NATIVE",
...facts,
});
nativeCall.mockImplementation(() => {
throw cause;
});
let observed: unknown;
try {
WindowsPrivateRuntime.create("D:/selected parent", "selected-prefix-");
} catch (failure: unknown) {
observed = failure;
}
expect(nativeCall).toHaveBeenCalledTimes(1);
expect(nativeCall).toHaveBeenCalledWith("runtime_create", [
"D:/selected parent",
"selected-prefix-",
]);
if (retryable) {
expect(observed).toBeInstanceOf(PrivateRuntimeRootUnavailableError);
expect(observed).toMatchObject({
message: cause.message,
cause,
retryable: true,
});
} else expect(observed).toBe(cause);
},
);
it("does not infer a transient status from a human-readable diagnostic", () => {
const cause = new Error("Open Windows path component failed (Win32 32)");
nativeCall.mockImplementation(() => {
throw cause;
});
let observed: unknown;
try {
WindowsPrivateRuntime.create("D:/selected parent", "selected-prefix-");
} catch (failure: unknown) {
observed = failure;
}
expect(observed).toBe(cause);
});