From b7911b2d30b9612969390e2d6a7bcbd7a24e67bf Mon Sep 17 00:00:00 2001 From: ShuaixinHuang <1836656514@qq.com> Date: Tue, 3 Nov 2026 09:38:01 +0800 Subject: [PATCH] fix(evm): explain limiter configuration and access failures (#8669) --- docs/evm-bytecode.md | 9 ++ src/evm/EvmoleInterfaceProvider.ts | 33 +++++- .../evm/evmLimiterAvailability.test.ts | 108 +++++++++++++----- .../evm/evmoleInterfaceProvider.test.ts | 2 +- 4 files changed, 121 insertions(+), 31 deletions(-) diff --git a/docs/evm-bytecode.md b/docs/evm-bytecode.md index 0b4d97bd65..e215f0028e 100644 --- a/docs/evm-bytecode.md +++ b/docs/evm-bytecode.md @@ -28,6 +28,15 @@ The host must already provide util-linux `prlimit`, normally `/usr/bin/prlimit`; no system tools and changes no user configuration. [Upstream provenance and license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md) are packaged. +Limiter discovery validates the selected path without launching it. A relative +path or directory is reported as `not_configured`, a missing file as +`executable_missing`, and denied access or another filesystem-probe failure as +`runtime_unavailable`. Recovery names the selected path and +`REA_EVM_PRLIMIT_COMMAND`; denied access asks for execute permission and +filesystem access. The original admission or system code stays in diagnostics. +An available filesystem probe does not verify the selected tool's version or +util-linux compatibility, and REA does not substitute another command. + ## Evidence semantics - `encoding` is required: raw bytes or UTF-8 hex, with optional `0x` prefix and diff --git a/src/evm/EvmoleInterfaceProvider.ts b/src/evm/EvmoleInterfaceProvider.ts index 2a72fef70d..4dfa48ba8e 100644 --- a/src/evm/EvmoleInterfaceProvider.ts +++ b/src/evm/EvmoleInterfaceProvider.ts @@ -1,4 +1,5 @@ import { EvmAdmissionError } from "./EvmAdmissionError.js"; +import type { ProviderRejectionCode } from "../domain/providerSelection.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { safeParseUtf8Json } from "../domain/safeJson.js"; @@ -115,6 +116,7 @@ type EvmLimiterSelection = { readonly diagnostics: Readonly>; }; type EvmLimiterUnavailable = EvmLimiterSelection & { + readonly code: ProviderRejectionCode; readonly reason: string; readonly cause: unknown; }; @@ -159,16 +161,39 @@ const selectEvmLimiter = async ( if (cause instanceof EvmAdmissionError) return err({ path, + code: "not_configured", diagnostics: { ...diagnostics, code: cause.code, ...cause.detail, }, - reason: cause.code, + reason: `Set REA_EVM_PRLIMIT_COMMAND to an absolute executable regular file for caller-supplied util-linux prlimit. The selected path ${path} ${cause.code === "prlimit_not_absolute" ? "is not absolute" : "is not a regular file"}.`, cause, }); - const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${path}; ${cause instanceof Error ? cause.message : String(cause)}`; - return err({ path, diagnostics, reason, cause }); + const systemCode = + cause instanceof Error && + "code" in cause && + typeof cause.code === "string" + ? cause.code + : undefined; + const permissionDenied = systemCode === "EACCES" || systemCode === "EPERM"; + const missing = systemCode === "ENOENT" || systemCode === "ENOTDIR"; + const recovery = permissionDenied + ? "Grant execute permission and filesystem access to the selected file, or set REA_EVM_PRLIMIT_COMMAND to an accessible absolute executable regular file for util-linux prlimit." + : missing + ? "Set REA_EVM_PRLIMIT_COMMAND to an existing absolute executable regular file for util-linux prlimit." + : "Check the selected file and filesystem access, or set REA_EVM_PRLIMIT_COMMAND to an accessible absolute executable regular file for util-linux prlimit."; + const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${path}; ${cause instanceof Error ? cause.message : String(cause)}. ${recovery}`; + return err({ + path, + code: missing ? "executable_missing" : "runtime_unavailable", + diagnostics: { + ...diagnostics, + ...(systemCode === undefined ? {} : { system_code: systemCode }), + }, + reason, + cause, + }); } }; @@ -193,7 +218,7 @@ export const evmLimiterAvailability = async ( } : { status: "unavailable", - code: "executable_missing", + code: selected.error.code, reason: selected.error.reason, // The limiter owner does not yet author a separate repair (#8508). remediation: selected.error.reason, diff --git a/tests/boundary/evm/evmLimiterAvailability.test.ts b/tests/boundary/evm/evmLimiterAvailability.test.ts index d8ad789adf..7684146571 100644 --- a/tests/boundary/evm/evmLimiterAvailability.test.ts +++ b/tests/boundary/evm/evmLimiterAvailability.test.ts @@ -1,4 +1,7 @@ import { expect, it } from "vitest"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs"; import { buildCapabilityInventory } from "../../../src/application/CapabilityInventory.js"; @@ -41,33 +44,43 @@ it("probes the default limiter with executable and regular-file checks", async ( ]); }); -it("uses the exact absolute override and reports a failed executable check", async () => { - const limiter = "/fixture/util-linux/prlimit"; - const availability = await evmLimiterAvailability( - { REA_EVM_PRLIMIT_COMMAND: limiter }, - { - ...linux, - fs: { - access: async (path) => { - expect(path).toBe(limiter); - throw Object.assign(new Error("permission denied"), { - code: "EACCES", - }); - }, - stat: async () => { - throw new Error("stat must not run after access failure"); +it.each(["EACCES", "EPERM"])( + "uses the exact absolute override and reports denied access: %s", + async (code) => { + const limiter = "/fixture/util-linux/prlimit"; + const availability = await evmLimiterAvailability( + { REA_EVM_PRLIMIT_COMMAND: limiter }, + { + ...linux, + fs: { + access: async (path) => { + expect(path).toBe(limiter); + throw Object.assign(new Error("permission denied"), { + code, + }); + }, + stat: async () => { + throw new Error("stat must not run after access failure"); + }, }, }, - }, - ); + ); - expect(availability).toMatchObject({ - status: "unavailable", - code: "executable_missing", - reason: expect.stringContaining(limiter), - diagnostics: { limiter_path: limiter }, - }); -}); + expect(availability).toMatchObject({ + status: "unavailable", + code: "runtime_unavailable", + reason: expect.stringContaining(limiter), + diagnostics: { limiter_path: limiter, system_code: code }, + }); + if (availability === undefined) throw new Error("Expected limiter probe"); + const entry = buildCapabilityInventory(status, { + ...basePolicy, + evmLimiterAvailability: availability, + }).find(({ name }) => name === "inspect_evm_interface"); + expect(entry?.remediation).toContain("REA_EVM_PRLIMIT_COMMAND"); + expect(entry?.remediation).toContain("execute permission"); + }, +); it("rejects a relative override and non-file without probing an alternate command", async () => { const relative = await evmLimiterAvailability( @@ -84,7 +97,8 @@ it("rejects a relative override and non-file without probing an alternate comman ); expect(relative).toMatchObject({ status: "unavailable", - reason: "prlimit_not_absolute", + code: "not_configured", + reason: expect.stringContaining("absolute executable regular file"), diagnostics: { limiter_path: "prlimit", code: "prlimit_not_absolute", @@ -104,11 +118,53 @@ it("rejects a relative override and non-file without probing an alternate comman ); expect(directory).toMatchObject({ status: "unavailable", - reason: "prlimit_not_file", + code: "not_configured", + reason: expect.stringContaining("absolute executable regular file"), diagnostics: expect.objectContaining({ code: "prlimit_not_file" }), }); }); +it("delivers selected-path recovery from real filesystem probes to capability output", async () => { + const root = await createTestTempDirectory("rea-evm-limiter-recovery-"); + const executable = join(root, "caller-selected-limiter"); + await writeFile(executable, "not executed by the readiness probe", { + mode: 0o700, + }); + for (const [path, expectedCode, expectedReason] of [ + ["prlimit", "not_configured", "absolute executable regular file"], + [root, "not_configured", "absolute executable regular file"], + [ + join(root, "missing-limiter"), + "executable_missing", + "REA_EVM_PRLIMIT_COMMAND", + ], + [executable, null, null], + ] as const) { + const availability = await evmLimiterAvailability( + { REA_EVM_PRLIMIT_COMMAND: path }, + linux, + ); + expect(availability?.code).toBe(expectedCode); + expect(availability?.diagnostics.limiter_path).toBe(path); + if (availability === undefined) throw new Error("Expected limiter probe"); + const entry = buildCapabilityInventory(status, { + ...basePolicy, + evmLimiterAvailability: availability, + }).find(({ name }) => name === "inspect_evm_interface"); + if (expectedReason === null) { + expect(entry).toMatchObject({ available: true, remediation: null }); + } else { + expect(entry).toMatchObject({ + available: false, + reason: "provider_missing", + }); + expect(entry?.remediation).toContain(expectedReason); + expect(entry?.remediation).toContain(path); + expect(entry?.remediation).toContain("util-linux prlimit"); + } + } +}); + it("rechecks on each observation so inventory follows the selected executable's current state", async () => { let accessible = true; const fs = { diff --git a/tests/boundary/evm/evmoleInterfaceProvider.test.ts b/tests/boundary/evm/evmoleInterfaceProvider.test.ts index 9edd10309e..1e98c38490 100644 --- a/tests/boundary/evm/evmoleInterfaceProvider.test.ts +++ b/tests/boundary/evm/evmoleInterfaceProvider.test.ts @@ -970,7 +970,7 @@ it.runIf(!unsupportedHost).each(["directory", "missing-interpreter"])( }); if (scenario === "directory") expect(result.error).toMatchObject({ - reason: "prlimit_not_file", + reason: expect.stringContaining("absolute executable regular file"), diagnostics: { limiter_path: limiter, code: "prlimit_not_file",