@@ -8,7 +8,159 @@ import { Fail, q, hideAndHardenFunction } from '@endo/errors';
88 * @import {PassStyle} from './types.js';
99 */
1010
11- const { getPrototypeOf, getOwnPropertyDescriptors, hasOwn, entries } = Object ;
11+ const {
12+ defineProperty,
13+ getPrototypeOf,
14+ getOwnPropertyDescriptors,
15+ getOwnPropertyDescriptor,
16+ hasOwn,
17+ entries,
18+ freeze,
19+ } = Object ;
20+
21+ const { apply } = Reflect ;
22+
23+ const hardenIsFake = ( ) => {
24+ // We do not trust isFrozen because lockdown with unsafe hardenTaming replaces
25+ // isFrozen with a version that is in cahoots with fake harden.
26+ const subject = harden ( { __proto__ : null , x : 0 } ) ;
27+ const desc = getOwnPropertyDescriptor ( subject , 'x' ) ;
28+ return desc ?. writable === true ;
29+ } ;
30+
31+ /**
32+ * Pass-style must defend its own integrity under a number of configurations.
33+ *
34+ * In all environments where we use pass-style, we can in principle rely on the
35+ * globalThis.TypeError and globalThis.Error to be safe.
36+ * We have similar code in SES that stands on the irreducible risk that an
37+ * attacker may run before SES, so the application must either ensure that SES
38+ * initializes first or that all prior code is benign.
39+ * For all other configurations, we rely to some degree on SES lockdown and a
40+ * Compartment for any measure of safety.
41+ *
42+ * Pass-style may be loaded by the host module system into the primary realm,
43+ * which the authors call the Start Compartment.
44+ * SES provides no assurances that any number of guest programs can be safely
45+ * executed by the host in the start compartment.
46+ * Such code must be executed in a guest compartment.
47+ * As such, it is irrelevant that the globalThis is mutable and also holds all
48+ * of the host's authority.
49+ *
50+ * Pass-style may be loaded into a guest compartment, and the globalThis of the
51+ * compartment may or may not be frozen.
52+ * We typically, as with importBundle, run every Node.js package in a dedicated
53+ * compartment with a gratuitiously frozen globalThis.
54+ * In this configuration, we can rely on globalThis.Error and
55+ * globalThis.TypeError to correspond to the realm's intrinsics, either because
56+ * the Compartment arranged for a frozen globalThis or because the pass-style
57+ * package provides no code that can arrange for a change to the compartment's
58+ * globalThis.
59+ *
60+ * Running multiple guests in a single compartment with an unfrozen globalThis
61+ * is incoherent and provides no assurance of mutual safety between those
62+ * guests.
63+ * No code, much less Pass-style, should be run in such a compartment.
64+ *
65+ * Although we can rely on the globalThis.Error and globalThis.TypeError
66+ * bindings, we can and do use `makeTypeError` to produce a TypeError instance
67+ * that is guaranteed to be an instance of the realm intrinsic by dint of
68+ * construction from language syntax.
69+ * The idiom "belt and suspenders" is well-known among the authors and means
70+ * gratuitous or redundant safety measures.
71+ * In this case, we wear both belt and suspenders *on our overalls*.
72+ *
73+ * @returns {TypeError }
74+ */
75+ const makeTypeError = ( ) => {
76+ try {
77+ // @ts -expect-error deliberate TypeError
78+ null . null ;
79+ throw TypeError ( 'obligatory' ) ; // To convince the type flow inferrence.
80+ } catch ( error ) {
81+ return error ;
82+ }
83+ } ;
84+
85+ export const makeRepairError = ( ) => {
86+ if ( ! hardenIsFake ( ) ) {
87+ return undefined ;
88+ }
89+
90+ const typeErrorStackDesc = getOwnPropertyDescriptor ( makeTypeError ( ) , 'stack' ) ;
91+ const errorStackDesc = getOwnPropertyDescriptor ( Error ( 'obligatory' ) , 'stack' ) ;
92+
93+ if (
94+ typeErrorStackDesc === undefined ||
95+ typeErrorStackDesc . get === undefined
96+ ) {
97+ return undefined ;
98+ }
99+
100+ if (
101+ errorStackDesc === undefined ||
102+ typeof typeErrorStackDesc . get !== 'function' ||
103+ typeErrorStackDesc . get !== errorStackDesc . get ||
104+ typeof typeErrorStackDesc . set !== 'function' ||
105+ typeErrorStackDesc . set !== errorStackDesc . set
106+ ) {
107+ // We have own stack accessor properties that are outside our expectations,
108+ // that therefore need to be understood better before we know how to repair
109+ // them.
110+ // See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_UNEXPECTED_ERROR_OWN_STACK_ACCESSOR.md
111+ throw TypeError (
112+ 'Unexpected Error own stack accessor functions (PASS_STYLE_UNEXPECTED_ERROR_OWN_STACK_ACCESSOR)' ,
113+ ) ;
114+ }
115+
116+ // We should otherwise only encounter this case on V8 and possibly immitators
117+ // like FaceBook's Hermes because of its problematic error own stack accessor
118+ // behavior, which creates an undeniable channel for communicating arbitrary
119+ // capabilities through the stack internal slot of arbitrary frozen objects.
120+ // Note that FF/SpiderMonkey, Moddable/XS, and the error stack proposal
121+ // all inherit a stack accessor property from Error.prototype, which is
122+ // great. That case needs no heroics to secure.
123+
124+ // In the V8 case as we understand it, all errors have an own stack accessor
125+ // property, but within the same realm, all these accessor properties have
126+ // the same getter and have the same setter.
127+ // This is therefore the case that we repair.
128+ //
129+ // Also, we expect tht the captureStackTrace proposal to create more cases
130+ // where error objects have own "stack" getters.
131+ // https://github.com/tc39/proposal-error-capturestacktrace
132+
133+ const feralStackGetter = freeze ( errorStackDesc . get ) ;
134+
135+ /** @param {unknown } error */
136+ const repairError = error => {
137+ // Only pay the overhead if it first passes this cheap isError
138+ // check. Otherwise, it will be unrepaired, but won't be judged
139+ // to be a passable error anyway, so will not be unsafe.
140+ const stackDesc = getOwnPropertyDescriptor ( error , 'stack' ) ;
141+ if (
142+ stackDesc &&
143+ stackDesc . get === feralStackGetter &&
144+ stackDesc . configurable
145+ ) {
146+ // Can only repair if it is configurable. Otherwise, leave
147+ // unrepaired, in which case it will not be judged passable,
148+ // avoiding a safety problem.
149+ defineProperty ( error , 'stack' , {
150+ // NOTE: Calls getter during harden, which seems dangerous.
151+ // But we're only calling the problematic getter whose
152+ // hazards we think we understand.
153+ value : apply ( feralStackGetter , error , [ ] ) ,
154+ } ) ;
155+ }
156+ } ;
157+ harden ( repairError ) ;
158+
159+ return repairError ;
160+ } ;
161+ harden ( makeRepairError ) ;
162+
163+ export const repairError = makeRepairError ( ) ;
12164
13165// TODO: Maintenance hazard: Coordinate with the list of errors in the SES
14166// whilelist.
@@ -178,6 +330,16 @@ export const confirmRecursivelyPassableError = (
178330 reject `Passable Error must inherit from an error class .prototype: ${ candidate } `
179331 ) ;
180332 }
333+ if ( repairError !== undefined ) {
334+ // This point is unreachable unless the candidate is mutable and the
335+ // platform is V8 or like V8 creates errors with an own "stack" getter or
336+ // setter, which would otherwise make them non-passable.
337+ // This should only occur with lockdown using unsafe hardenTaming or an
338+ // equivalent fake, non-actually-freezing harden.
339+ // Under these circumstances only, passStyleOf alters an object as a side
340+ // effect, converting the "stack" property to a data value.
341+ repairError ( candidate ) ;
342+ }
181343 const descs = getOwnPropertyDescriptors ( candidate ) ;
182344 if ( ! ( 'message' in descs ) ) {
183345 return (
@@ -197,9 +359,7 @@ export const confirmRecursivelyPassableError = (
197359} ;
198360harden ( confirmRecursivelyPassableError ) ;
199361
200- /**
201- * @type {PassStyleHelper }
202- */
362+ /** @type {PassStyleHelper } */
203363export const ErrorHelper = harden ( {
204364 styleName : 'error' ,
205365
0 commit comments