Conversation
|
I should note that I am not hoping for timely review of these changes. |
6883a5f to
07ac148
Compare
| @@ -0,0 +1,4 @@ | |||
| # Change Log | |||
|
|
|||
| All notable changes to this project will be documented in this file. | |||
There was a problem hiding this comment.
What is the scope of "project"? Do you mean "package"?
There was a problem hiding this comment.
Yes. I’ll fix uniformly in follow-up. This boilerplate is in every package.
packages/harden/README.md
Outdated
| For common usage, this package provides an implementation of `harden` that will | ||
| freeze an object and its transitive properties, making them shallowly | ||
| immutable. | ||
| If used in composition with Lockdown in [HardenedJS](https://hardenedjs.org), |
There was a problem hiding this comment.
| If used in composition with Lockdown in [HardenedJS](https://hardenedjs.org), | |
| If used in composition with `lockdown` in [HardenedJS](https://hardenedjs.org), |
packages/harden/README.md
Outdated
|
|
||
| In this example, we create a hadened module. | ||
| That is, the module's surface and any value reachable by interacting with its | ||
| behaviors transitively produces values that sare safe to share with guest code. |
There was a problem hiding this comment.
| behaviors transitively produces values that sare safe to share with guest code. | |
| behaviors transitively produces values that are safe to share with guest code. |
packages/harden/README.md
Outdated
| # harden | ||
|
|
||
| For common usage, this package provides an implementation of `harden` that will | ||
| freeze an object and its transitive properties, making them shallowly |
There was a problem hiding this comment.
| freeze an object and its transitive properties, making them shallowly | |
| freeze an object and its transitive own properties, making them shallowly |
Additional pedantic detail that can be omitted in a high level summary, but needs to be explained somewhere. It does a "transitive reflective" walk of properties. The difference is that avoids invoking getters. Rather it proceeds to walk both getters and setters as objects.
| // We should only encounter this case on v8 because of its problematic | ||
| // error own stack accessor behavior. |
There was a problem hiding this comment.
We should cite Jordan's recent Error stack proposal.
| * | ||
| * @type {(() => any) | undefined} | ||
| */ | ||
| export const FERAL_STACK_GETTER = feralStackGetter; |
There was a problem hiding this comment.
Nice that you pulled these out here. However, should the ses error handling also be pulled out into a separate low level module, they may need to coordinate here. Not sure.
|
|
||
| // Obtain the string tag accessor of of TypedArray so we can indirectly use the | ||
| // TypedArray brand check it employs. | ||
| const typedArrayToStringTag = getOwnPropertyDescriptor( |
There was a problem hiding this comment.
Separately, I have been thinking that once Immutable ArrayBuffers are in the language and adequately deployed, we should retire harden's special case for TypedArrays. If someone wants to harden a TypedArray, they should start with a frozen TypedArray.
erights
left a comment
There was a problem hiding this comment.
I should note that I am not hoping for timely review of these changes.
Well, you got one anyway. LGTM!
|
Agreement out-of-band from @mhofman: There is a hazard that an object So, I can introduce a coarse mechanism to maintain the partition of lockdown and no-lockdown hardening (that can be frustrated by eval twins, but eval twins are simply frustrating). It involves a single bit of module scope state for which I hope to be forgiven. |
|
I also want to change the harden semantics to only verify that the prototype has been previously (or concurrently) hardened under lockdown. In non lockdown it would not check the prototype. This is a breaking change and I would prefer if it happened at the same time or before to avoid having to incur a future breaking change of the standalone harden (unless we reify this as a 3rd configuration state for the prototype traversal option) |
|
I think that change can be made additive, even with the current framing. I don’t expose the |
1188569 to
39f68f6
Compare
There was a problem hiding this comment.
The
hardenprovided pre-lockdown differs only in that it does not visit prototypes.
🥳
The
hardenandintrinsicspackages remain inside the SES TCB and are linted according to the same stricter restrictions for access to free variables and use of polymorphic calls.
Where do we document the scope of the SES TCB? Ideally, it should be trivial to look up and verify constraints on all transitive dependencies from the SES package itself, and also to identify any specific package as belonging to that collection and verify the presence of expected constraints. This is relevant for e.g. #2822, because the proposed cache-map is also in the trusted compute base.
Comments that must be resolved before merging
| // Needed only for the Safari bug workaround below | ||
| const { defineProperty: originalDefineProperty } = Object; | ||
|
|
||
| export const defineProperty = (object, prop, descriptor) => { | ||
| // We used to do the following, until we had to reopen Safari bug | ||
| // https://bugs.webkit.org/show_bug.cgi?id=222538#c17 | ||
| // Once this is fixed, we may restore it. | ||
| // // Object.defineProperty is allowed to fail silently so we use | ||
| // // Object.defineProperties instead. | ||
| // return defineProperties(object, { [prop]: descriptor }); | ||
|
|
||
| // Instead, to workaround the Safari bug | ||
| const result = originalDefineProperty(object, prop, descriptor); | ||
| if (result !== object) { | ||
| // See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_DEFINE_PROPERTY_FAILED_SILENTLY.md | ||
| throw TypeError( | ||
| `Please report that the original defineProperty silently failed to set ${stringifyJson( | ||
| String(prop), | ||
| )}. (SES_DEFINE_PROPERTY_FAILED_SILENTLY)`, | ||
| ); | ||
| } | ||
| return result; | ||
| }; |
There was a problem hiding this comment.
https://bugs.webkit.org/show_bug.cgi?id=222538 was marked resolved in 2021; it's probably time to either drop the workaround or update its explanation.
There was a problem hiding this comment.
On this one, I'm suspicious. So we should test before we assume that they "resolved" it because they actually fixed it. A one-time manual test would be fine.
| * uncurryThis() | ||
| * Equivalent of: fn => (that, ...args) => apply(fn, that, args) | ||
| * | ||
| * See those reference for a complete explanation: |
There was a problem hiding this comment.
| * See those reference for a complete explanation: | |
| * See this reference for a complete explanation: |
| /** @type {<T, U>(that: readonly T[], callbackfn: (value: T, index: number, array: T[]) => U, cbThisArg?: any) => U[]} */ | ||
| export const arrayMap = /** @type {any} */ (uncurryThis(arrayPrototype.map)); | ||
| export const arrayFlatMap = /** @type {any} */ ( | ||
| uncurryThis(arrayPrototype.flatMap) | ||
| ); | ||
| export const arrayPop = uncurryThis(arrayPrototype.pop); | ||
| /** @type {<T>(that: T[], ...items: T[]) => number} */ | ||
| export const arrayPush = uncurryThis(arrayPrototype.push); |
There was a problem hiding this comment.
How much typing information do we want here? I ask because #2730 includes the Object.entries/Object.fromEntries analog to this, which could just be brought all the way down.
| /** | ||
| * @type { & | ||
| * ((that: string, searchValue: { [Symbol.replace](string: string, replaceValue: string): string; }, replaceValue: string) => string) & | ||
| * ((that: string, searchValue: { [Symbol.replace](string: string, replacer: (substring: string, ...args: any[]) => string): string; }, replacer: (substring: string, ...args: any[]) => string) => string) | ||
| * } | ||
| */ | ||
| export const stringReplace = /** @type {any} */ ( | ||
| uncurryThis(stringPrototype.replace) | ||
| ); |
There was a problem hiding this comment.
Is this intentionally excluding strings from the domain of searchValue? If so, we should comment to that effect (e.g., "This typing intentionally constrains searchValue to the regular expression interface, because at call sites it is too easy to overlook that use of a string is limited to one replacement."). Or if not, we should fix the typing:
| /** | |
| * @type { & | |
| * ((that: string, searchValue: { [Symbol.replace](string: string, replaceValue: string): string; }, replaceValue: string) => string) & | |
| * ((that: string, searchValue: { [Symbol.replace](string: string, replacer: (substring: string, ...args: any[]) => string): string; }, replacer: (substring: string, ...args: any[]) => string) => string) | |
| * } | |
| */ | |
| export const stringReplace = /** @type {any} */ ( | |
| uncurryThis(stringPrototype.replace) | |
| ); | |
| /** | |
| * @type { & | |
| * ((that: string, searchValue: ({ [Symbol.replace](string: string, replaceValue: string): string; } | string), replaceValue: string) => string) & | |
| * ((that: string, searchValue: ({ [Symbol.replace](string: string, replacer: (substring: string, ...args: any[]) => string): string; } | string), replacer: (substring: string, ...args: any[]) => string) => string) | |
| * } | |
| */ | |
| export const stringReplace = /** @type {any} */ ( | |
| uncurryThis(stringPrototype.replace) | |
| ); |
There was a problem hiding this comment.
I’m choosing to walk past this since this is a refactor, and I don’t think we need a more expressive type since this is currently internal to ses and can be relaxed without breaking later.
packages/harden/src/make-hardener.js
Outdated
| /** | ||
| * @param {any} guard | ||
| * @returns {asserts guard} | ||
| */ | ||
| const assert = guard => { | ||
| if (!guard) { | ||
| throw new TypeError('assertion failed'); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Minor suggestion:
| /** | |
| * @param {any} guard | |
| * @returns {asserts guard} | |
| */ | |
| const assert = guard => { | |
| if (!guard) { | |
| throw new TypeError('assertion failed'); | |
| } | |
| }; | |
| /** @type {(condition: any) => asserts condition} */ | |
| const assert = condition => { | |
| if (!condition) { | |
| throw new TypeError('assertion failed'); | |
| } | |
| }; |
packages/harden/src/make-hardener.js
Outdated
| * | ||
| * @returns {Harden} | ||
| * @template T | ||
| * @param {boolean} ascendPrototypeChains |
There was a problem hiding this comment.
Interesting; I've always thought of prototype chains in the other direction (such that following them would be descent rather than ascent).
| * @param {boolean} ascendPrototypeChains | |
| * @param {boolean} traversePrototypeChains |
packages/harden/src/make-hardener.js
Outdated
| // The harden implementation exported by @endo/harden does not ascend | ||
| // prototype chains but may be imported into programs after lockdown. | ||
| // In this case, the weak hardener must give way to the strong hardener | ||
| // on the global object, lazily. |
There was a problem hiding this comment.
| // The harden implementation exported by @endo/harden does not ascend | |
| // prototype chains but may be imported into programs after lockdown. | |
| // In this case, the weak hardener must give way to the strong hardener | |
| // on the global object, lazily. | |
| // The harden implementation exported by @endo/harden does not traverse | |
| // prototype chains but may be imported into programs after lockdown, or | |
| // before lockdown for later use. | |
| // In these cases, the weak hardener must give way to the strong hardener | |
| // on the global object, and to support the latter | |
| // (`import { harden } from "@endo/harden"; import "@endo/init";`), it must do | |
| // so lazily. |
| let { harden } = { | ||
| /** | ||
| * @template T | ||
| * @param {T} root | ||
| * @returns {T} | ||
| */ | ||
| harden(root) { |
There was a problem hiding this comment.
Suggestion: simplify.
| let { harden } = { | |
| /** | |
| * @template T | |
| * @param {T} root | |
| * @returns {T} | |
| */ | |
| harden(root) { | |
| /** @type {<T,>(root: T) => T} */ | |
| let harden = root => { |
packages/harden/src/make-hardener.js
Outdated
| ({ harden } = (innerHarden => ({ | ||
| /** | ||
| * @template T | ||
| * @param {T} root | ||
| * @returns {T} | ||
| */ | ||
| harden(root) { | ||
| // Use a native hardener if possible. | ||
| if (typeof globalThis.harden === 'function') { | ||
| const globalHarden = globalThis.harden; | ||
| return globalHarden(root); | ||
| } else { | ||
| return innerHarden(root); | ||
| } | ||
| }, | ||
| }))(harden)); |
There was a problem hiding this comment.
Suggestion: simplify.
| ({ harden } = (innerHarden => ({ | |
| /** | |
| * @template T | |
| * @param {T} root | |
| * @returns {T} | |
| */ | |
| harden(root) { | |
| // Use a native hardener if possible. | |
| if (typeof globalThis.harden === 'function') { | |
| const globalHarden = globalThis.harden; | |
| return globalHarden(root); | |
| } else { | |
| return innerHarden(root); | |
| } | |
| }, | |
| }))(harden)); | |
| const innerHarden = harden; | |
| harden = root => { | |
| // Use a global hardener if possible. | |
| const globalHarden = globalThis.harden; | |
| if (typeof globalHarden === 'function' && globalHarden !== harden) { | |
| return globalHarden(root); | |
| } | |
| return innerHarden(root); | |
| }; |
There was a problem hiding this comment.
But I must admit, this harden being vulnerable to future globalThis manipulation seems to fly in the face of our goals. What if it were instead a one-time sensitivity?
| ({ harden } = (innerHarden => ({ | |
| /** | |
| * @template T | |
| * @param {T} root | |
| * @returns {T} | |
| */ | |
| harden(root) { | |
| // Use a native hardener if possible. | |
| if (typeof globalThis.harden === 'function') { | |
| const globalHarden = globalThis.harden; | |
| return globalHarden(root); | |
| } else { | |
| return innerHarden(root); | |
| } | |
| }, | |
| }))(harden)); | |
| let innerHarden = harden; | |
| harden = root => { | |
| // Capture the global hardener (or lack thereof) on first invocation, and | |
| // use it if present. | |
| if (mode === undefined) { | |
| mode = ascendPrototypeChains; | |
| const globalHarden = globalThis.harden; | |
| if (typeof globalHarden === 'function' && globalHarden !== harden) { | |
| innerHarden = globalHarden; | |
| } | |
| } | |
| return innerHarden(root); | |
| }; |
or
| ({ harden } = (innerHarden => ({ | |
| /** | |
| * @template T | |
| * @param {T} root | |
| * @returns {T} | |
| */ | |
| harden(root) { | |
| // Use a native hardener if possible. | |
| if (typeof globalThis.harden === 'function') { | |
| const globalHarden = globalThis.harden; | |
| return globalHarden(root); | |
| } else { | |
| return innerHarden(root); | |
| } | |
| }, | |
| }))(harden)); | |
| let innerHarden = harden; | |
| let globalHarden; | |
| harden = root => { | |
| // Capture the global hardener (or lack thereof) on first invocation, | |
| // require it to remain constant, and use it if present. | |
| if (mode === undefined) { | |
| mode = ascendPrototypeChains; | |
| globalHarden = globalThis.harden; | |
| if (typeof globalHarden === 'function' && globalHarden !== harden) { | |
| innerHarden = globalHarden; | |
| } | |
| } | |
| if (globalThis.harden !== globalHarden) { | |
| throw new TypeError('a global hardener must not be replaced'); | |
| } | |
| return innerHarden(root); | |
| }; |
naugtur
left a comment
There was a problem hiding this comment.
please release a version of SES before merging this
898a587 to
f880999
Compare
|
Toward reviving this stale branch, I had a conversation out-of-band with @mhofman that uncovered a hazard we want to address before merging this change as designed. That is, if a hardened module initializes before lockdown, it may be in a state where its prototypes are unfrozen until the first instance is hardened. This is related to an orthogonal hazard of having modules that explicitly support both shallow hardening before lockdown and deep hardening after lockdown. We do not believe we can in good conscience relax post-lockdown hardening so that it throws an exception if it encounters an unhardened prototype instead of implicitly capturing it in the closure of to-be-hardened objects. This would create a hazard where a module uses shallow pre-lockdown harden for its own partial defense but is incompatible with a post-lockdown mode because it neglected to pre-harden its prototypes. As a condition of proceeding with this design @mhofman proposes that we add a lockdown mode that issues warnings when harden encounters an unhardened prototype, in order to softly encourage code to pre-harden prototypes and eliminate a window between creation and instantiation where an attacker can manipulate an exported prototype. We currently have a mechanism in place that causes a pre-lockdown harden to throw an exception if it is called again post-lockdown, but should add a mechanism that causes repairIntrinsics to throw an exception if it is called after a pre-lockdown harden was called. (It occurs to me as I summarize this, that the latter mechanism may obviate the former.) This new mechanism has some requirements:
We also wish to encourage code going forward to adopt It occurs to me as I summarize that the simplest formulation of this problem is that, between eval twins of the pre-lockdown harden and the installation of a post-lockdown harden, we can and should only ever use one implementation of these. For pre-lockdown hardens, this is not a race to initialization, but a race to first-use, with the additional requirement that lockdown should fail if it loses the race. @mhofman proposes a mechanism that will universally cause lockdown to fail if repairIntrinsics occurs after any pre-lockdown harden gets used: the installation of a non-writable non-configurable property on an intrinsic, e.g., Object.@@harden. Proposed changeSo, the proposed design is to have pre-lockdown hardens and hardenIntrinsics itself race to install a As a courtesy to developers, we add explicit code to repairIntrinsics that detects I believe we have the option of removing the trap that detects a change to |
erights
left a comment
There was a problem hiding this comment.
Just requesting changes in order to turn off my approval in light of upcoming big changes. I want to be sure to review again after these big changes but before merge. I do not currently have any changes in mind to request.
|
For the record, I am of the opinion that the user-facing |
This was actually how harden worked before my time at Agoric, and I became uneasy with the arbitrary boundary between the “frontier” that must be kept permanently in sync between “harden” and “lockdown”. It is not actually possible for us, at least at this time, to commit to an extent in “lockdown” that will not grow, such that an old “harden” and a new “lockdown” would fail to coördinate at the point of repairs. |
I wanted to make them behave the same before and after lockdown, but in the opposite direction: that neither would check the prototypes. It would then become a matter for other layers like pass-style to enforce that prototype chains are hardened, which means that layer would now need to be lockdown aware. It also meant that it was easier for code wanting to harden to do an insufficient thing if another layer like pass-style was never involved. I think that having different behaviors pre and post lockdown is the only way to reconcile this. I would like to offer better diagnostics to pre-lockdown harden users (not just post-lockdown), and this can potentially be done by harden pre-lockdown building a list of primordials to not warn about, but since it's for diagnostics only, that would be fine (it could also warn if it is hardening some primordial object, even pointing users to lockdown itself) |
@kriskowal I would like to hear more about this «old “harden” and a new “lockdown”» scenario. Right now
@mhofman What would be the benefit of totally ignoring prototypes like that? |
|
As much for my own edification, with propositions labeled for reference:
The dimensions to the design of a
Discussion of variants:
Tangentially, the regarding whether
My tentative preference is:
|
|
Closing with intention to instead pursue #2976 |
Refs: #2983, #1686
Reviewer instructions
This change introduces two packages, with implementation factored out of
sesand lightly modified. To highlight those modifications, the commits are individually reviewable. In separate commits, I have copied certain modules to new locations verbatim, then applied changes in place. For example, theses/commons.jseffectively moves out to another package, but parts stay insesand other parts move toharden/commons.jssince they are not otherwise shared. Thehardentests are duplicated and lightly edited, since the intended behavior is similar before and after lockdown.Description
This change introduces an
@endo/hardenpackage that provides the implementation ofhardenforsesbut also a relaxed implementation ofhardensuitable beforelockdown, for modules that should interoperate between HardenedJS and non-HardenedJS. The expectation is that we will use@endo/hardenin most of Endo, likemarshalandpass-style, to make CapTP usable with or without Lockdown.Security Considerations
The
hardenprovided pre-lockdown differs only in that it does not visit prototypes. Thehardenandintrinsicspackages remain inside the SES TCB and are linted according to the same stricter restrictions for access to free variables and use of polymorphic calls.Hardening before and after lockdown should converge on the same state.
Scaling Considerations
Hardening before and after lockdown should converge on the same state, but redundantly.
Hardening before lockdown and after lockdown use distinct tables for tracking prior work.
Documentation Considerations
The new modules have minimal README documentation. Further documentation for the
hardenpackage in particular should follow.Testing Considerations
Tests preserved and copied with the appropriate variation for pre-lockdown.
Compatibility Considerations
Compatibility is preserved.
The
hardenandintrinsicspackages may be versioned on an independent cadence ofsessince they are not directly exposed. That is,intrinsicsmay make breaking changes to its API without breakingses.The
intrinsicspackage may or may not be a suitable foundation for a futureget-intrinsicsshim which we expect to be entangled with the internals of the SESpermits. Future designs will determine how permits and the get intrinsics shims get packaged and it is our hope that this structure does not impede progress in that direction.Upgrade Considerations
None.