mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
fix: report provider admission failures as typed codes (#8066)
* fix: report provider admission failures as typed codes * fix(evm): keep limiter path on admission diagnostics * fix: assert WABT and CBOR admission codes * fix: keep admission details on returned failures * fix(evm): detect repeated CBOR keys in the decoder --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
3b647132c6
commit
317bf3fe23
63 files changed
+1021
-432
No files matched your search
@@ -0,0 +1,31 @@
|
||||
const EVM_ADMISSION_CODES = [
|
||||
"prlimit_not_absolute",
|
||||
"prlimit_not_file",
|
||||
"cleanup_incomplete",
|
||||
"cleanup_unconfirmed",
|
||||
"marker_mismatch",
|
||||
"request_path_missing",
|
||||
"tag_or_float",
|
||||
"text_bytes_unavailable",
|
||||
"collection_exceeds_bound",
|
||||
"nesting_exceeds_profile",
|
||||
"value_outside_profile",
|
||||
"repeated_map_key",
|
||||
"root_not_map",
|
||||
] as const;
|
||||
|
||||
export type EvmAdmissionCode = (typeof EVM_ADMISSION_CODES)[number];
|
||||
|
||||
/** EVM limiter, worker, or metadata admission failure. `code` is the stable identity. */
|
||||
export class EvmAdmissionError extends Error {
|
||||
readonly _tag = "EvmAdmissionError";
|
||||
|
||||
constructor(
|
||||
readonly code: EvmAdmissionCode,
|
||||
readonly detail?: Readonly<Record<string, string | number>>,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(code, options);
|
||||
this.name = "EvmAdmissionError";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EvmAdmissionError } from "./EvmAdmissionError.js";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { safeParseUtf8Json } from "../domain/safeJson.js";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { access, readFile, stat, writeFile } from "node:fs/promises";
|
||||
@@ -110,7 +112,7 @@ export interface EvmLimiterProbeOptions {
|
||||
|
||||
type EvmLimiterSelection = {
|
||||
readonly path: string;
|
||||
readonly diagnostics: Readonly<Record<string, string>>;
|
||||
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
||||
};
|
||||
type EvmLimiterUnavailable = EvmLimiterSelection & {
|
||||
readonly reason: string;
|
||||
@@ -142,20 +144,29 @@ const selectEvmLimiter = async (
|
||||
const diagnostics = { limiter_path: path, platform, arch };
|
||||
try {
|
||||
if (!isAbsolute(path))
|
||||
throw new Error(
|
||||
"REA_EVM_PRLIMIT_COMMAND must be an absolute executable path.",
|
||||
);
|
||||
throw new EvmAdmissionError("prlimit_not_absolute", {
|
||||
setting: "REA_EVM_PRLIMIT_COMMAND",
|
||||
});
|
||||
const fs = probes?.fs ?? { access, stat };
|
||||
await fs.access(path, constants.X_OK);
|
||||
throwIfAborted();
|
||||
if (!(await fs.stat(path)).isFile())
|
||||
throw new Error(
|
||||
"Configured prlimit must be a regular executable file, not a directory.",
|
||||
);
|
||||
throw new EvmAdmissionError("prlimit_not_file");
|
||||
throwIfAborted();
|
||||
return ok({ path, diagnostics });
|
||||
} catch (cause: unknown) {
|
||||
throwIfAborted();
|
||||
if (cause instanceof EvmAdmissionError)
|
||||
return err({
|
||||
path,
|
||||
diagnostics: {
|
||||
...diagnostics,
|
||||
code: cause.code,
|
||||
...cause.detail,
|
||||
},
|
||||
reason: cause.code,
|
||||
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 });
|
||||
}
|
||||
@@ -452,10 +463,13 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
|
||||
stopped.status === "incomplete" ||
|
||||
stopped.status === "not-owned"
|
||||
)
|
||||
throw new Error(
|
||||
throw new EvmAdmissionError(
|
||||
stopped.status === "incomplete"
|
||||
? stopped.reason
|
||||
: "Owned EVM worker cleanup did not confirm ownership.",
|
||||
? "cleanup_incomplete"
|
||||
: "cleanup_unconfirmed",
|
||||
stopped.status === "incomplete"
|
||||
? { reason: stopped.reason }
|
||||
: undefined,
|
||||
{ cause: stopped },
|
||||
);
|
||||
},
|
||||
@@ -511,6 +525,7 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
|
||||
selectedLimiter.error.reason,
|
||||
{
|
||||
cause: selectedLimiter.error.cause,
|
||||
diagnostics: selectedLimiter.error.diagnostics,
|
||||
userMessage: selectedLimiter.error.reason,
|
||||
},
|
||||
);
|
||||
@@ -813,9 +828,7 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
|
||||
marker = completed;
|
||||
}
|
||||
if (!marker.bytes.equals(Buffer.from("F")))
|
||||
throw new Error(
|
||||
"Private worker failure marker does not match the observed exit status.",
|
||||
);
|
||||
throw new EvmAdmissionError("marker_mismatch");
|
||||
fileFailure = { verified: true, failure: null };
|
||||
} catch (markerFailure: unknown) {
|
||||
fileFailure = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EvmAdmissionError } from "./EvmAdmissionError.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { z } from "zod";
|
||||
@@ -23,7 +24,7 @@ class UnsupportedContainer extends Error {}
|
||||
const main = async (): Promise<void> => {
|
||||
const requestPath = process.argv[2];
|
||||
if (requestPath === undefined)
|
||||
throw new Error("Owned worker request path missing.");
|
||||
throw new EvmAdmissionError("request_path_missing");
|
||||
const request = requestSchema.parse(
|
||||
JSON.parse(await readFile(requestPath, "utf8")),
|
||||
);
|
||||
|
||||
@@ -163,11 +163,28 @@ it("rejects repeated deeply nested container keys promptly", () => {
|
||||
inspectSolidityMetadata(bytecode("a2" + key + "01" + key + "02")),
|
||||
).toMatchObject({
|
||||
status: "not-decoded",
|
||||
diagnostic: expect.stringContaining("found repeat map key"),
|
||||
diagnostic: "repeated_map_key",
|
||||
admission: {
|
||||
code: "repeated_map_key",
|
||||
detail: { key_length: expect.any(Number) },
|
||||
},
|
||||
});
|
||||
expect(performance.now() - started).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
it("records the profile depth when CBOR nesting is refused", () => {
|
||||
expect(
|
||||
inspectSolidityMetadata(bytecode("a16178" + "81".repeat(65) + "00")),
|
||||
).toMatchObject({
|
||||
status: "not-decoded",
|
||||
diagnostic: "nesting_exceeds_profile",
|
||||
admission: {
|
||||
code: "nesting_exceeds_profile",
|
||||
detail: { max_depth: 64 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["", "60006000f3", "000000", "00010001"])(
|
||||
"does not claim absent Solidity metadata for an unavailable trailer: %s",
|
||||
(hex) =>
|
||||
|
||||
+67
-23
@@ -1,3 +1,4 @@
|
||||
import { EvmAdmissionError } from "./EvmAdmissionError.js";
|
||||
import { decode, Tokenizer, Type, type Token } from "cborg";
|
||||
import type {
|
||||
SolidityMetadata,
|
||||
@@ -14,39 +15,52 @@ const decodeOptions = {
|
||||
allowUndefined: false,
|
||||
allowInfinity: false,
|
||||
allowNaN: false,
|
||||
rejectDuplicateMapKeys: true,
|
||||
rejectDuplicateMapKeys: false,
|
||||
retainStringBytes: true,
|
||||
} as const;
|
||||
|
||||
/** Bound recursive decoder entry before collection interpretation/allocation. */
|
||||
class BoundedMetadataTokenizer extends Tokenizer {
|
||||
readonly remaining: number[] = [];
|
||||
readonly #mapKeys: Array<Set<string> | undefined> = [];
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes, decodeOptions);
|
||||
}
|
||||
|
||||
override next(): Token {
|
||||
while (this.remaining.at(-1) === 0) this.remaining.pop();
|
||||
while (this.remaining.at(-1) === 0) {
|
||||
this.remaining.pop();
|
||||
this.#mapKeys.pop();
|
||||
}
|
||||
const token = super.next();
|
||||
if (
|
||||
Type.equals(token.type, Type.tag) ||
|
||||
Type.equals(token.type, Type.float)
|
||||
)
|
||||
throw new Error(
|
||||
"CBOR tags and floating-point values are outside this metadata profile.",
|
||||
);
|
||||
throw new EvmAdmissionError("tag_or_float");
|
||||
if (Type.equals(token.type, Type.string)) {
|
||||
if (token.byteValue !== undefined)
|
||||
token.value = utf8.decode(token.byteValue);
|
||||
// cborg answers the shortest-form empty text (0x60) with a shared token that has no
|
||||
// bytes; it can only mean "", which is already its value.
|
||||
else if (token.value !== "" || token.encodedLength !== 1)
|
||||
throw new Error(
|
||||
"CBOR text bytes were unavailable for strict UTF-8 validation.",
|
||||
);
|
||||
throw new EvmAdmissionError("text_bytes_unavailable");
|
||||
}
|
||||
const parent = this.remaining.length - 1;
|
||||
const mapKeys = parent >= 0 ? this.#mapKeys[parent] : undefined;
|
||||
const onKey =
|
||||
mapKeys !== undefined && parent >= 0 && this.remaining[parent]! % 2 === 0;
|
||||
if (parent >= 0) this.remaining[parent]!--;
|
||||
if (
|
||||
onKey &&
|
||||
mapKeys !== undefined &&
|
||||
!Type.equals(token.type, Type.map) &&
|
||||
!Type.equals(token.type, Type.array)
|
||||
) {
|
||||
const identity = keyIdentity(token.value);
|
||||
if (mapKeys.has(identity)) throw repeatedMapKey(identity);
|
||||
mapKeys.add(identity);
|
||||
}
|
||||
if (
|
||||
Type.equals(token.type, Type.map) ||
|
||||
Type.equals(token.type, Type.array)
|
||||
@@ -61,11 +75,14 @@ class BoundedMetadataTokenizer extends Tokenizer {
|
||||
children < 0 ||
|
||||
children > this.data.length
|
||||
)
|
||||
throw new Error(
|
||||
"Declared CBOR collection exceeds the encoded item-byte bound.",
|
||||
);
|
||||
throw new EvmAdmissionError("collection_exceeds_bound");
|
||||
if (this.remaining.length >= MAX_DEPTH)
|
||||
throw new Error("CBOR nesting exceeds the 64-level metadata profile.");
|
||||
throw new EvmAdmissionError("nesting_exceeds_profile", {
|
||||
max_depth: MAX_DEPTH,
|
||||
});
|
||||
this.#mapKeys.push(
|
||||
Type.equals(token.type, Type.map) ? new Set() : undefined,
|
||||
);
|
||||
this.remaining.push(children);
|
||||
}
|
||||
return token;
|
||||
@@ -85,9 +102,7 @@ const scalar = (value: unknown): SolidityMetadataScalar => {
|
||||
if (value instanceof Map) return { kind: "uninterpreted", container: "map" };
|
||||
if (Array.isArray(value))
|
||||
return { kind: "uninterpreted", container: "array" };
|
||||
throw new Error(
|
||||
"CBOR decoder returned a value outside the admitted profile.",
|
||||
);
|
||||
throw new EvmAdmissionError("value_outside_profile");
|
||||
};
|
||||
|
||||
/** Value identity for duplicate-key checks; map entry order is not significant. */
|
||||
@@ -101,14 +116,21 @@ const keyIdentity = (value: unknown): string => {
|
||||
return JSON.stringify(scalar(value));
|
||||
};
|
||||
|
||||
const repeatedMapKey = (identity: string): EvmAdmissionError =>
|
||||
new EvmAdmissionError(
|
||||
"repeated_map_key",
|
||||
identity.length <= 128
|
||||
? { key: identity }
|
||||
: { key_length: identity.length },
|
||||
);
|
||||
|
||||
/** Check every map boundary, even when nested values remain uninterpreted. */
|
||||
const validateMapKeys = (value: unknown): void => {
|
||||
if (value instanceof Map) {
|
||||
const seen = new Set<string>();
|
||||
for (const [key, item] of value) {
|
||||
const identity = keyIdentity(key);
|
||||
if (seen.has(identity))
|
||||
throw new Error(`CBOR decode error: found repeat map key ${identity}`);
|
||||
if (seen.has(identity)) throw repeatedMapKey(identity);
|
||||
seen.add(identity);
|
||||
validateMapKeys(key);
|
||||
validateMapKeys(item);
|
||||
@@ -118,6 +140,32 @@ const validateMapKeys = (value: unknown): void => {
|
||||
}
|
||||
};
|
||||
|
||||
/** Keep the typed code and its detail; map only the decoder's duplicate-key throw. */
|
||||
const solidityFailure = (
|
||||
cause: unknown,
|
||||
): {
|
||||
readonly diagnostic: string;
|
||||
readonly admission?: {
|
||||
readonly code: string;
|
||||
readonly detail?: Readonly<Record<string, string | number>>;
|
||||
};
|
||||
} => {
|
||||
if (cause instanceof EvmAdmissionError)
|
||||
return {
|
||||
diagnostic: cause.code,
|
||||
admission: {
|
||||
code: cause.code,
|
||||
...(cause.detail === undefined ? {} : { detail: cause.detail }),
|
||||
},
|
||||
};
|
||||
return {
|
||||
diagnostic: (cause instanceof Error ? cause.message : String(cause)).slice(
|
||||
0,
|
||||
512,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/** Parse only a plausible terminal root-map candidate; never establish absence. */
|
||||
export const inspectSolidityMetadata = (
|
||||
bytes: Uint8Array,
|
||||
@@ -148,8 +196,7 @@ export const inspectSolidityMetadata = (
|
||||
...decodeOptions,
|
||||
tokenizer: new BoundedMetadataTokenizer(selected),
|
||||
});
|
||||
if (!(raw instanceof Map))
|
||||
throw new Error("Terminal CBOR candidate did not decode to a root map.");
|
||||
if (!(raw instanceof Map)) throw new EvmAdmissionError("root_not_map");
|
||||
validateMapKeys(raw);
|
||||
// cborg compares Map keys by identity, so repeated byte-string or container
|
||||
// keys (each its own object) pass its duplicate check; compare by value at
|
||||
@@ -185,10 +232,7 @@ export const inspectSolidityMetadata = (
|
||||
status: "not-decoded",
|
||||
entries: [],
|
||||
compiler_version: null,
|
||||
diagnostic: (cause instanceof Error
|
||||
? cause.message
|
||||
: String(cause)
|
||||
).slice(0, 512),
|
||||
...solidityFailure(cause),
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user