-
Notifications
You must be signed in to change notification settings - Fork 74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: prepare for non-trapping integrity trait #2679
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -13,7 +13,7 @@ const harden = /** @type {import('ses').Harden & { isFake?: boolean }} */ ( | |||||
global.harden | ||||||
); | ||||||
|
||||||
const { getPrototypeOf, defineProperty } = Object; | ||||||
const { getPrototypeOf, defineProperty, freeze } = Object; | ||||||
const { ownKeys } = Reflect; | ||||||
|
||||||
test('passStyleOf basic success cases', t => { | ||||||
|
@@ -193,16 +193,22 @@ test('passStyleOf testing remotables', t => { | |||||
|
||||||
const tagRecord1 = harden(makeTagishRecord('Alleged: manually constructed')); | ||||||
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ | ||||||
const farObj1 = harden({ | ||||||
__proto__: tagRecord1, | ||||||
}); | ||||||
const farObj1 = harden({ __proto__: tagRecord1 }); | ||||||
t.is(passStyleOf(farObj1), 'remotable'); | ||||||
|
||||||
const tagRecord2 = makeTagishRecord('Alleged: tagRecord not hardened'); | ||||||
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ | ||||||
const farObj2 = Object.freeze({ | ||||||
__proto__: tagRecord2, | ||||||
}); | ||||||
/** | ||||||
* TODO In order to run this test before we have explicit support for a | ||||||
* non-trapping integrity trait, we have to `freeze` here but not `harden`. | ||||||
* However, once we do have that support, and `passStyleOf` checks that | ||||||
* its argument is also non-trapping, we still need to avoid `harden` | ||||||
* because that would also hardden `__proto__`. So we will need to | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
* explicitly make this non-trapping, which we cannot yet express. | ||||||
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md | ||||||
Comment on lines
+200
to
+207
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I was confused at first, but this is a test that explicitly verifies that an object with a non-hardened proto is not passable. Can we make that more clear in the prelude of this comment? |
||||||
* | ||||||
* @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 | ||||||
*/ | ||||||
const farObj2 = freeze({ __proto__: tagRecord2 }); | ||||||
if (harden.isFake) { | ||||||
t.is(passStyleOf(farObj2), 'remotable'); | ||||||
} else { | ||||||
|
@@ -212,39 +218,27 @@ test('passStyleOf testing remotables', t => { | |||||
}); | ||||||
} | ||||||
|
||||||
const tagRecord3 = Object.freeze( | ||||||
makeTagishRecord('Alleged: both manually frozen'), | ||||||
); | ||||||
const tagRecord3 = harden(makeTagishRecord('Alleged: both manually frozen')); | ||||||
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ | ||||||
const farObj3 = Object.freeze({ | ||||||
__proto__: tagRecord3, | ||||||
}); | ||||||
const farObj3 = harden({ __proto__: tagRecord3 }); | ||||||
Comment on lines
-215
to
+223
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should get a TODO to be switched to both manually "non-trapping" instead of using |
||||||
t.is(passStyleOf(farObj3), 'remotable'); | ||||||
|
||||||
const tagRecord4 = harden(makeTagishRecord('Remotable')); | ||||||
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ | ||||||
const farObj4 = harden({ | ||||||
__proto__: tagRecord4, | ||||||
}); | ||||||
const farObj4 = harden({ __proto__: tagRecord4 }); | ||||||
t.is(passStyleOf(farObj4), 'remotable'); | ||||||
|
||||||
const tagRecord5 = harden(makeTagishRecord('Not alleging')); | ||||||
const farObj5 = harden({ | ||||||
__proto__: tagRecord5, | ||||||
}); | ||||||
const farObj5 = harden({ __proto__: tagRecord5 }); | ||||||
t.throws(() => passStyleOf(farObj5), { | ||||||
message: | ||||||
/For now, iface "Not alleging" must be "Remotable" or begin with "Alleged: " or "DebugName: "; unimplemented/, | ||||||
}); | ||||||
|
||||||
const tagRecord6 = harden(makeTagishRecord('Alleged: manually constructed')); | ||||||
const farObjProto6 = harden({ | ||||||
__proto__: tagRecord6, | ||||||
}); | ||||||
const farObjProto6 = harden({ __proto__: tagRecord6 }); | ||||||
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ | ||||||
const farObj6 = harden({ | ||||||
__proto__: farObjProto6, | ||||||
}); | ||||||
const farObj6 = harden({ __proto__: farObjProto6 }); | ||||||
t.is(passStyleOf(farObj6), 'remotable', 'tagRecord grandproto is accepted'); | ||||||
|
||||||
// Our current agoric-sdk plans for far classes are to create a class-like | ||||||
|
@@ -323,12 +317,8 @@ test('passStyleOf testing remotables', t => { | |||||
const fauxTagRecordB = harden( | ||||||
makeTagishRecord('Alleged: manually constructed', harden({})), | ||||||
); | ||||||
const farObjProtoB = harden({ | ||||||
__proto__: fauxTagRecordB, | ||||||
}); | ||||||
const farObjB = harden({ | ||||||
__proto__: farObjProtoB, | ||||||
}); | ||||||
const farObjProtoB = harden({ __proto__: fauxTagRecordB }); | ||||||
const farObjB = harden({ __proto__: farObjProtoB }); | ||||||
t.throws(() => passStyleOf(farObjB), { | ||||||
message: | ||||||
'cannot serialize Remotables with non-methods like "Symbol(passStyle)" in "[Alleged: manually constructed]"', | ||||||
|
@@ -387,7 +377,16 @@ test('remotables - safety from the gibson042 attack', t => { | |||||
}, | ||||||
); | ||||||
|
||||||
const makeInput = () => Object.freeze({ __proto__: mercurialProto }); | ||||||
/** | ||||||
* TODO In order to run this test before we have explicit support for a | ||||||
* non-trapping integrity trait, we have to `freeze` here but not `harden`. | ||||||
* However, once we do have that support, and `passStyleOf` checks that | ||||||
* its argument is also non-trapping, we still need to avoid `harden` | ||||||
* because that would also hardden `__proto__`. So we will need to | ||||||
* explicitly make this non-trapping, which we cannot yet express. | ||||||
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md | ||||||
*/ | ||||||
const makeInput = () => freeze({ __proto__: mercurialProto }); | ||||||
const input1 = makeInput(); | ||||||
const input2 = makeInput(); | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Preparing for the Non-trapping Integrity Trait | ||
|
||
The [Stabilize proposal](https://github.com/tc39/proposal-stabilize) is currently at stage 1 of the tc39 process. It proposes three distinct integrity traits whose current placeholder names are: | ||
- ***fixed***: would mitigate the return-override mistake by preventing objects with this trait from being stamped with new class-private-fields. | ||
- ***overridable***: would mitigate the assignment-override mistake by enabling non-writable properties inherited from an object with this trait to be overridden by property assignment on an inheriting object. | ||
- ***non-trapping***: would mitigate proxy-based reentrancy hazards by having a proxy whose target carries this trait never trap to its handler, but rather just perform the default action directly on this non-trapping target. | ||
|
||
Draft PR [feat(non-trapping-shim): ponyfill and shim for the non-trapping integrity trait #2673](https://github.com/endojs/endo/pull/2673) is a ponyfill and shim for this non-trapping integrity trait. The names it introduces are placeholders, since the bikeshedding process for these names has not yet concluded. | ||
|
||
Draft PR [feat(ses,pass-style): use non-trapping integrity trait for safety #2675](https://github.com/endojs/endo/pull/2675) uses this support for the non-trapping integity trait to mitigate reentrancy attacks from hardened objects, expecially passable copy-data objects like copyLists, copyRecords, and taggeds. To do so, it makes two fundamental changes: | ||
- Where `harden` made the object at every step frozen, that PR changes `harden` to also make those objects non-trapping. | ||
- Where `passStyleOf` checked that objects are frozen, that PR changes `passStyleOf` to also check that those objects are non-trapping. | ||
|
||
## How proxy code should prepare | ||
|
||
[#2673](https://github.com/endojs/endo/pull/2673) will *by default* produce proxies that refuse to be made non-trapping. An explicit handler trap (whose name is TBD) will need to be explicitly provided to make a proxy that allows itself to be made non-trapping. This is the right default, because proxies on frozen almost-empty objects can still have useful trap behavior for their `get`, `set`, `has`, and `apply` traps. Even on a frozen target | ||
- The `get`, `set`, and `has` traps applied to a non-own property name are still general traps that can have useful trapping behavior. | ||
- The `apply` trap can ignore the target's call behavior and just do its own thing. | ||
|
||
However, to prepare for these changes, we need to avoid hardening both such proxies and their targets. We need to avoid hardening their target because this will bypass the traps. We need to avoid hardening the proxy because such proxies will *by default* refuse to be made non-trapping, and thus refuse to be hardened. | ||
|
||
## How passable objects should prepare | ||
|
||
Although we think of `passStyleOf` as requiring its input to be hardened, `passStyleOf` instead checked that each relevant object is frozen. Manually freezing all objects reachable from a root object had been equivalent to hardening that root object. With these changes, even such manual transitive freezing will not make an object passable. To prepare for these changes, use `harden` explicitly instead. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah I didn't think of that. That will potentially create a decent amount of churn on code that was manually freezing. It would be so much easier if frozen regular object would automatically pass trapping checks, but that would effectively transform non-trapping into a "is proxy" predicate. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,12 +7,18 @@ import { | |
freeze, | ||
getOwnPropertyDescriptors, | ||
globalThis, | ||
immutableObject, | ||
} from './commons.js'; | ||
import { assert } from './error/assert.js'; | ||
|
||
const { Fail, quote: q } = assert; | ||
|
||
/** | ||
* `freeze` but not `harden` the proxy target so it remains trapping. | ||
* Thus, it should not be shared outside this module. | ||
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md | ||
*/ | ||
const onlyFrozenObject = freeze(create(null)); | ||
|
||
/** | ||
* alwaysThrowHandler | ||
* This is an object that throws if any property is called. It's used as | ||
|
@@ -21,7 +27,7 @@ const { Fail, quote: q } = assert; | |
* create one and share it between all Proxy handlers. | ||
*/ | ||
export const alwaysThrowHandler = new Proxy( | ||
immutableObject, | ||
onlyFrozenObject, | ||
freeze({ | ||
get(_shadow, prop) { | ||
Fail`Please report unexpected scope handler trap: ${q(String(prop))}`; | ||
Comment on lines
32
to
33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So thinking about this, unless we do something wrong inside ses itself, we should never trigger this for the |
||
|
@@ -88,6 +94,6 @@ export const strictScopeTerminatorHandler = freeze( | |
); | ||
|
||
export const strictScopeTerminator = new Proxy( | ||
immutableObject, | ||
onlyFrozenObject, | ||
strictScopeTerminatorHandler, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe mention it's safe to share these targets between multiple E proxy instances because they're never actually used?