diff --git a/packages/env-options/src/env-options.js b/packages/env-options/src/env-options.js index b0047eee15..ae4d3e5fb0 100644 --- a/packages/env-options/src/env-options.js +++ b/packages/env-options/src/env-options.js @@ -61,22 +61,21 @@ export const makeEnvironmentCaptor = aGlobal => { /** @type {string} */ let setting = defaultSetting; - const globalProcess = aGlobal.process; - if (globalProcess && typeof globalProcess === 'object') { - const globalEnv = globalProcess.env; - if (globalEnv && typeof globalEnv === 'object') { - if (optionName in globalEnv) { - arrayPush(capturedEnvironmentOptionNames, optionName); - const optionValue = globalEnv[optionName]; - // eslint-disable-next-line @endo/no-polymorphic-call - typeof optionValue === 'string' || - Fail`Environment option named ${q( - optionName, - )}, if present, must have a corresponding string value, got ${q( - optionValue, - )}`; - setting = optionValue; - } + const globalProcess = aGlobal.process || undefined; + const globalEnv = + (typeof globalProcess === 'object' && globalProcess.env) || undefined; + if (typeof globalEnv === 'object') { + if (optionName in globalEnv) { + arrayPush(capturedEnvironmentOptionNames, optionName); + const optionValue = globalEnv[optionName]; + // eslint-disable-next-line @endo/no-polymorphic-call + typeof optionValue === 'string' || + Fail`Environment option named ${q( + optionName, + )}, if present, must have a corresponding string value, got ${q( + optionValue, + )}`; + setting = optionValue; } } return setting; diff --git a/packages/ses/src/error/fatal-assert.js b/packages/ses/src/error/fatal-assert.js index 219144be80..29d1d0dc18 100644 --- a/packages/ses/src/error/fatal-assert.js +++ b/packages/ses/src/error/fatal-assert.js @@ -10,7 +10,7 @@ let abandon; // below). Currently it only checks for the `process.abort` or `process.exit` // found on Node. It should also sniff for a vat terminating function expected // to be found within the start compartment of SwingSet vats. What else? -if (typeof process === 'object') { +if (typeof process === 'object' && process) { abandon = process.abort || process.exit; } let raise; diff --git a/packages/ses/src/error/tame-console.js b/packages/ses/src/error/tame-console.js index 000f08207f..33fac1c122 100644 --- a/packages/ses/src/error/tame-console.js +++ b/packages/ses/src/error/tame-console.js @@ -1,6 +1,7 @@ // @ts-check import { + // Using TypeError minimizes risk of exposing the feral Error constructor TypeError, apply, defineProperty, @@ -13,6 +14,10 @@ import { makeRejectionHandlers } from './unhandled-rejection.js'; import './types.js'; import './internal-types.js'; +const failFast = message => { + throw TypeError(message); +}; + const wrapLogger = (logger, thisArg) => freeze((...args) => apply(logger, thisArg, args)); @@ -60,9 +65,9 @@ export const tameConsole = ( unhandledRejectionTrapping = 'report', optGetStackString = undefined, ) => { - if (consoleTaming !== 'safe' && consoleTaming !== 'unsafe') { - throw TypeError(`unrecognized consoleTaming ${consoleTaming}`); - } + consoleTaming === 'safe' || + consoleTaming === 'unsafe' || + failFast(`unrecognized consoleTaming ${consoleTaming}`); let loggedErrorHandler; if (optGetStackString === undefined) { @@ -95,21 +100,36 @@ export const tameConsole = ( /* eslint-disable @endo/no-polymorphic-call */ // Node.js - if (errorTrapping !== 'none' && globalThis.process !== undefined) { - globalThis.process.on('uncaughtException', error => { + const globalProcess = globalThis.process || undefined; + if ( + errorTrapping !== 'none' && + typeof globalProcess === 'object' && + typeof globalProcess.on === 'function' + ) { + let terminate; + if (errorTrapping === 'platform' || errorTrapping === 'exit') { + const { exit } = globalProcess; + // If there is a function-valued process.on but no function-valued process.exit, + // fail early without caring whether errorTrapping is "platform" only by default. + typeof exit === 'function' || failFast('missing process.exit'); + terminate = () => exit(globalProcess.exitCode || -1); + } else if (errorTrapping === 'abort') { + terminate = globalProcess.abort; + typeof terminate === 'function' || failFast('missing process.abort'); + } + + globalProcess.on('uncaughtException', error => { // causalConsole is born frozen so not vulnerable to method tampering. ourConsole.error(error); - if (errorTrapping === 'platform' || errorTrapping === 'exit') { - globalThis.process.exit(globalThis.process.exitCode || -1); - } else if (errorTrapping === 'abort') { - globalThis.process.abort(); + if (terminate) { + terminate(); } }); } - if ( unhandledRejectionTrapping !== 'none' && - globalThis.process !== undefined + typeof globalProcess === 'object' && + typeof globalProcess.on === 'function' ) { const handleRejection = reason => { // 'platform' and 'report' just log the reason. @@ -119,32 +139,32 @@ export const tameConsole = ( const h = makeRejectionHandlers(handleRejection); if (h) { // Rejection handlers are supported. - globalThis.process.on('unhandledRejection', h.unhandledRejectionHandler); - globalThis.process.on('rejectionHandled', h.rejectionHandledHandler); - globalThis.process.on('exit', h.processTerminationHandler); + globalProcess.on('unhandledRejection', h.unhandledRejectionHandler); + globalProcess.on('rejectionHandled', h.rejectionHandledHandler); + globalProcess.on('exit', h.processTerminationHandler); } } // Browser + const globalWindow = globalThis.window || undefined; if ( errorTrapping !== 'none' && - globalThis.window !== undefined && - globalThis.window.addEventListener !== undefined + typeof globalWindow === 'object' && + typeof globalWindow.addEventListener === 'function' ) { - globalThis.window.addEventListener('error', event => { + globalWindow.addEventListener('error', event => { event.preventDefault(); // 'platform' and 'report' just log the reason. ourConsole.error(event.error); if (errorTrapping === 'exit' || errorTrapping === 'abort') { - globalThis.window.location.href = `about:blank`; + globalWindow.location.href = `about:blank`; } }); } - if ( unhandledRejectionTrapping !== 'none' && - globalThis.window !== undefined && - globalThis.window.addEventListener !== undefined + typeof globalWindow === 'object' && + typeof globalWindow.addEventListener === 'function' ) { const handleRejection = reason => { ourConsole.error('SES_UNHANDLED_REJECTION:', reason); @@ -153,17 +173,17 @@ export const tameConsole = ( const h = makeRejectionHandlers(handleRejection); if (h) { // Rejection handlers are supported. - globalThis.window.addEventListener('unhandledrejection', event => { + globalWindow.addEventListener('unhandledrejection', event => { event.preventDefault(); h.unhandledRejectionHandler(event.reason, event.promise); }); - globalThis.window.addEventListener('rejectionhandled', event => { + globalWindow.addEventListener('rejectionhandled', event => { event.preventDefault(); h.rejectionHandledHandler(event.promise); }); - globalThis.window.addEventListener('beforeunload', _event => { + globalWindow.addEventListener('beforeunload', _event => { h.processTerminationHandler(); }); } diff --git a/packages/ses/src/tame-domains.js b/packages/ses/src/tame-domains.js index d14e07a884..7b5adcbe4c 100644 --- a/packages/ses/src/tame-domains.js +++ b/packages/ses/src/tame-domains.js @@ -17,12 +17,10 @@ export function tameDomains(domainTaming = 'safe') { } // Protect against the hazard presented by Node.js domains. - if (typeof globalThis.process === 'object' && globalThis.process !== null) { + const globalProcess = globalThis.process || undefined; + if (typeof globalProcess === 'object') { // Check whether domains were initialized. - const domainDescriptor = getOwnPropertyDescriptor( - globalThis.process, - 'domain', - ); + const domainDescriptor = getOwnPropertyDescriptor(globalProcess, 'domain'); if (domainDescriptor !== undefined && domainDescriptor.get !== undefined) { // The domain descriptor on Node.js initially has value: null, which // becomes a get, set pair after domains initialize. @@ -37,7 +35,7 @@ export function tameDomains(domainTaming = 'safe') { // The domain module merely throws an exception when it attempts to define // the domain property of the process global during its initialization. // We have no better recourse because Node.js uses defineProperty too. - defineProperty(globalThis.process, 'domain', { + defineProperty(globalProcess, 'domain', { value: null, configurable: false, writable: false, diff --git a/packages/ses/test/test-lockdown-shimmed-process.js b/packages/ses/test/test-lockdown-shimmed-process.js new file mode 100644 index 0000000000..2465043e9d --- /dev/null +++ b/packages/ses/test/test-lockdown-shimmed-process.js @@ -0,0 +1,22 @@ +/* global globalThis */ + +import test from 'ava'; +import '../index.js'; + +test('shimmed globalThis.process', t => { + const process = {}; + Object.defineProperty(globalThis, 'process', { + value: process, + configurable: false, + writable: false, + }); + t.is(globalThis.process, process); + t.is(globalThis.process.on, undefined); + lockdown({ + consoleTaming: 'safe', + errorTrapping: 'platform', + unhandledRejectionTrapping: 'report', + }); + t.is(globalThis.process, process); + t.is(globalThis.process.on, undefined); +});