diff --git a/packages/examples/cli-demo/src/cli.ts b/packages/examples/cli-demo/src/cli.ts index 3a8fecc7a8..68c9598e6c 100644 --- a/packages/examples/cli-demo/src/cli.ts +++ b/packages/examples/cli-demo/src/cli.ts @@ -91,12 +91,27 @@ class CliInterruptedError extends Error { } } +/** Render an arbitrary value without trusting its type traps or string coercion. */ +function renderUnknown(value: unknown): string { + try { + return String(value) + } catch { + return '[unrenderable thrown value]' + } +} + +/** Normalize an arbitrary thrown value without letting inspection escape containment. */ function toError(error: unknown): Error { - return error instanceof Error ? error : new Error(String(error)) + try { + if (error instanceof Error) return error + } catch { + // A hostile proxy may throw during instanceof; use the total renderer below. + } + return new Error(renderUnknown(error)) } function interruptionReason(signal: AbortSignal): string { - return signal.reason === undefined ? 'interrupted' : String(signal.reason) + return signal.reason === undefined ? 'interrupted' : renderUnknown(signal.reason) } /** diff --git a/packages/examples/cli-demo/tests/cli.spec.ts b/packages/examples/cli-demo/tests/cli.spec.ts index 8c41236829..fe61a42304 100644 --- a/packages/examples/cli-demo/tests/cli.spec.ts +++ b/packages/examples/cli-demo/tests/cli.spec.ts @@ -199,6 +199,27 @@ describe('runOneShot and executeCli', () => { expect(stderr).toContain('boot exploded') }) + it('contains a thrown value whose inspection and coercion both fail', async () => { + const hostile = new Proxy({}, { + getPrototypeOf: () => { throw new Error('prototype trap escaped') }, + get: (target, key, receiver) => { + if (key === Symbol.toPrimitive) throw new Error('coercion escaped') + return Reflect.get(target, key, receiver) as unknown + }, + }) + let stdout = '' + let stderr = '' + const code = await executeCli(['task'], { + boot: async () => { throw hostile }, + loadEnv: () => {}, + writeStdout: (chunk) => { stdout += chunk }, + writeStderr: (chunk) => { stderr += chunk }, + }) + expect(code).toBe(1) + expect(stdout).toBe('') + expect(stderr).toBe('dsh-cli-demo: [unrenderable thrown value]\n') + }) + it('interrupts Loader boot and contains every late boot outcome', async () => { const abort = new AbortController() const lateContext = new Context()