From d3305887cb12253e29f8af00a20824f28dc03da4 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:26:54 +0800 Subject: [PATCH] fix: reject PTY environment names the backend cannot preserve --- docs/process-capture.md | 5 +++ src/domain/process/processScenario.test.ts | 42 ++++++++++++++++++++++ src/domain/process/processScenario.ts | 33 ++++++++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/docs/process-capture.md b/docs/process-capture.md index be1d755c2f..5b43d4967b 100644 --- a/docs/process-capture.md +++ b/docs/process-capture.md @@ -12,6 +12,11 @@ sandbox. Filesystem observation paths select what REA snapshots; they do not restrict what the process can read or write. The inherited environment is not recorded, so its influence may remain unknown. +The installed PTY backend cannot preserve an explicit environment name +`__proto__`. REA rejects that override before launch; use `/usr/bin/env` with +`__proto__=VALUE` as an argument when that literal variable is required. The +reserved `REA_PROCESS_RUN_ID` name also remains unavailable to scenario overrides. + ## Host support Capturing a new scenario currently requires Linux or macOS and a working diff --git a/src/domain/process/processScenario.test.ts b/src/domain/process/processScenario.test.ts index 450d873673..a8f8e1046e 100644 --- a/src/domain/process/processScenario.test.ts +++ b/src/domain/process/processScenario.test.ts @@ -1,4 +1,6 @@ import { expect, it } from "vitest"; +import { z } from "zod"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { SESSION_TOOL_CONTRACTS } from "../../contracts/sessionToolContracts.js"; import { @@ -12,6 +14,23 @@ import { const baseScenario = { executable: "/usr/bin/true" }; +it("advertises the same unsupported PTY environment name as runtime admission", () => { + const schema = z.toJSONSchema(processScenarioSchema.shape.environment, { + io: "input", + }); + const ajv = new Ajv2020({ strict: false }); + const validate = ajv.compile(schema); + expect(validate(Object.fromEntries([["__proto__", "selected"]]))).toBe(false); + expect( + validate({ + constructor: "ordinary", + prototype: "ordinary", + NORMAL_CAPTURE_KEY: "control", + }), + ).toBe(true); + expect(validate({ REA_PROCESS_RUN_ID: "reserved" })).toBe(false); +}); + it("defaults the finalization interval to zero without changing committed identity", () => { const scenario = parseProcessScenario(baseScenario); @@ -237,3 +256,26 @@ it("identifies each event scheduled after the scenario timeout", () => { }), ]); }); + +it.each(["selected-value", 7])( + "rejects a selected __proto__ PTY environment value instead of omitting %j", + (value) => { + const environment = Object.fromEntries([ + ["__proto__", value], + ["NORMAL_CAPTURE_KEY", "control"], + ]); + const parsed = processScenarioSchema.safeParse({ + ...baseScenario, + environment, + }); + expect(parsed.success).toBe(false); + if (parsed.success) + throw new Error("Expected the unsupported PTY name to be rejected"); + expect(parsed.error.issues).toContainEqual( + expect.objectContaining({ + path: ["environment", "__proto__"], + message: expect.stringContaining("PTY backend cannot preserve"), + }), + ); + }, +); diff --git a/src/domain/process/processScenario.ts b/src/domain/process/processScenario.ts index 42dd3684d8..6378fa715f 100644 --- a/src/domain/process/processScenario.ts +++ b/src/domain/process/processScenario.ts @@ -45,12 +45,43 @@ const environmentName = z ? `${reservedRunIdEnvironmentName} is reserved by the process adapter` : "Environment names cannot contain '=' or NUL", }); +const unsupportedPtyEnvironmentName = "__proto__"; +const ptyEnvironmentName = environmentName.regex( + environmentNamePattern(unsupportedPtyEnvironmentName), + { + error: + "The installed PTY backend cannot preserve the __proto__ environment name; pass it through /usr/bin/env instead.", + }, +); const childProcessString = z .string() .regex( /^[^\x00]*$/u, "Values passed to operating-system APIs cannot contain NUL", ); +const environmentRecordSchema = z.record( + ptyEnvironmentName, + childProcessString, +); +// z.record intentionally skips this key, so validate its presence before parsing. +const processEnvironmentSchema = z + .unknown() + .superRefine((value, context) => { + if ( + value !== null && + typeof value === "object" && + Object.hasOwn(value, unsupportedPtyEnvironmentName) + ) + context.addIssue({ + code: "custom", + path: [unsupportedPtyEnvironmentName], + message: + "The installed PTY backend cannot preserve the __proto__ environment name; pass it through /usr/bin/env instead.", + }); + }) + .pipe(environmentRecordSchema); +processEnvironmentSchema._zod.toJSONSchema = () => + z.toJSONSchema(environmentRecordSchema, { io: "input" }); const fitsProcessTerminalCellBudget = ( columns: number, @@ -153,7 +184,7 @@ export const processScenarioSchema = z executable: childProcessString.min(1), arguments: z.array(childProcessString).default([]), working_directory: childProcessString.default("."), - environment: z.record(environmentName, childProcessString).default({}), + environment: processEnvironmentSchema.default({}), filesystem_observation_paths: z .array(childProcessString.min(1)) .default([]),