-
Notifications
You must be signed in to change notification settings - Fork 80
Description
What is the Problem Being Solved?
Endo provides many “hardened” modules. These are modules that use harden defensively and, because ses reveals globalThis.harden only after lockdown, these hardened modules can only run in a locked-down environment.
These modules, particularly @endo/marshal, are useful in environments in which the difficulty exceeds the value of locking down, like web frontend properties where the value of lockdown is limited to cases where they also elect to use LavaMoat to mitigate supply chain attacks. The constraints of the lockdown environment interact with complications introduced by popular frontend frameworks.
This issue subsumes terminating CapTP without lockdown, but referenced for the useful design discussion: #1686
Option: Refactor SES such that @endo/harden exports a version that can be used before lockdown but doesn’t visit prototypes
- Harden before Lockdown #2782
- refactor: Decouple passable stack from lockdown with @endo/harden #2830
Option: Provide an @endo/harden that is a no-op by default with build-condition and lockdown-option opt-in for alternatives
- feat(@endo/harden): Lightweight pre-lockdown hardened module support #2976
- feat: All hardened modules work without lockdown #2978
Option: Provide a @endo/harden which is superficial or freezes the “surface” of objects by default, with opt-in for alternatives.
? What should the *default* no-lockdown behavior of `harden` be when imported from `@endo/harden`, such that any other alternative is opt-in with `-C` conditions. Regardless of which option is the default, `bundle-source -C hardened` will provide a minimal-overhead implementation of `@endo/harden` that presumes that it will run in a locked-down environment, or throws upon initialization.
: No freezing, a.k.a., pinky-swear immutability of pass-style.
+ Fast
+ Small (very little overhead to bundle sizes)
+ And therefore much less likely to grow existing bundles beyond size limits by default.
- No enforcement of purported immutability of pass-style.
- Either rejection by Endo patterns, or similar weakening of `matches`/`mustMatch`
: Freezing the transitive closure of properties.
+ Safe to the extent that own properties are immutable
- But unsafe to the extent that prototype properties that are not overshadowed are mutable
- Entrains a copy or variant of the SES hardening machinery in bundles, which is likely to cause bundles to grow beyond their size limit.
+ Which can be mitigated with an opt-out to the no-freeze or expect global harden variant with a `-C condition`
: Freezing the transitive closure of properties and prototypes.
+ Entrains a copy of the SES hardening machinery in bundles.
- Renders it impossible to subsequently call lockdown.
+ But we have committed to a position where it is not possible to lockdown if you use harden before calling lockdown
Description of the Design
TBD
Security Considerations
TBD
Scaling Considerations
TBD
Test Plan
TBD
Compatibility Considerations
TBD
Upgrade Considerations
TBD
Design considerations
As much for my own edification, with propositions labeled for reference:
- I start from the (urgent) premise that it be possible to join an ocap network without lockdown or oblige the application author to coordinate the order of installation of shims. To that end,
- Suppose that we partition all valid programs into those that use a pre-lockdown
hardenand those that use a post-lockdownharden, and any application that tries to do both fails when they try tolockdown. - To ensure A, suppose that co-tenant
@endo/hardenimplementations race to install non-writable, non-configurableObject[Symbol.for('harden')]upon the first invocation ofharden. Any instance of@endo/hardenis eligible to win the race and determines the semantics (and holder of the memo), and the losers silently tolerate losing the race. Whereas, iflockdownloses the race, it throws an exception. - We accept this is not the usual strategy for shims, which lately tend to encourage overwriting what was found, because there is a possibility applications will rely on the behavior of the shim and break if a future version of the language behaves differently than the shim predicted. We also accept that this kind of shimming is weirdly compatible with HardenedJS because it tolerates a previously locked-down environment.
- The
@endo/hardenmodule would fail upon first attempt tohardeninside aCompartmentin composition with a version of SES that does not installObject[Symbol.for('harden')]and whereglobalThis.hardenwas not endowed. No such versions ofsescurrently exist, as compartments receivehardenby default. No future version ofseswould break because it will introduceObject[Symbol.for('harden')]which cannot be denied by the creator of aCompartment. It is a shared intrinsic that cannot be omitted from aCompartmentexcept under extreme duress. - To ensure that
@endo/hardenworks with versions of SES that predate the decision to installObject[Symbol.for('harden')], it will also sense (upon first invocation) that it lost the race if it instead findsglobalThis.hardenis installed, and will fall-through to that instead. - There would be no versions of
@endo/hardenthat predate the choice to installObject[Symbol.for('harden')], so no mutual compatibility concerns among versions of@endo/harden. - Using the
"exports"directive in the@endo/hardenpackage.json, we can let the user of an application or the creator of a bundle elect a mode ofhardenthat it will use if it wins the race. So, we can choose a default mode and elective alternate modes. Electing an alternative mode would look likebundle-source -C shallow-harden - The mode most useful for bundling applications that can presume that they are executed in a locked-down environment would simply omit its own implementation of
hardenand use the one installed atglobalThis.hardenorObject[Symbol.for('harden')]. That would look likebundle-source -C lockded-down. So, bundles that migrate from assuming the existence of a post-lockdownglobalThis.hardencan migrate to use@endo/harden, become more portable, without incurring a significant bundle size increase. - For every mode of
@endo/harden, the mode variant only applies to the pre-lockdown behavior. - Ideally, a library that is built to use
@endo/hardendoes not need to separately test its behavior pre- and post-lockdown. If it works with pre-lockdown, it should not break post-lockdown. - All post-lockdown
hardenoptions must guarantee that the transitive closure of both prototypes and properties are frozen, because callinghardenshould relieve the reviewer of any concern of reachable transitive mutability, even for code that might be run pre-lockdown. - Regardless of whether
hardencloses over prototypes when hardening instances, a library that does not immediately and explicitly harden a prototype will be vulnerable to mutation in coordination with other linked modules until the first instance is hardened, which may be considerably later, so we ideally provide some signal or encouragement to opportunisticallyharden.
The dimensions to the design of a harden implementation are:
- pre vs post
- harden extent the extent that
hardenfreezes - throwing extent the extent in which
hardenwill throw if it finds an unfrozen object - warning extent the extent in which
hardenwill warn if it finds an unfrozen object - for each extent, whether it close over:
- surface transitive closure over properties of an object
- volume transitive closure over both properties and prototypes of an object
- global
globalThis - undeniable intrinsics intrinsics that can’t be denied because they’re reachable with syntax, like
AsyncFunction.prototype. - shared intrinsics the shared intrinsics, a set that may grow version over version
- special intrinsics intrinsics that are reachable by calling methods of shared intrinsics, a set that may grow version over version
harden variants of interest.
- fake harden which returns the subject unaltered
- assuring harden for which the harden extent is the volume of the subject (only current behavior)
- throwing harden for which the harden extent is the surface of the subject and otherwise the throwing extent is the volume of the subject.
- warning harden for which the harden extent is the surface and otherwise the warning extent is the volume
- harden until intrinsics for which the harden extent is the volume of the subject except for the volume of shared intrinsics
- harden until global for which the harden extent is the volume of the subject except for the volume of global and special intrinsics
Discussion of variants:
- assuring harden would not be eligible before
lockdownbecause it hardens shared intrinsics in a way that would interfere withlockdown, and also can interfere with shims of anything captured by a hardened instance. It’s technically at peace withlockdownbecause (2) ensures that usinghardenbeforelockdownforeswears ever callinglockdown. But, aggressively hardening prototypes down to the intrinsics is inconsistent with the values of a pre-lockdown application. - warning harden pre-lockdown entrains complications about reporting warnings, because we will want knobs to mute or redirect those warnings, which will have to be on
process.env. This isn’t a deal-breaker. - warning harden post-lockdown runs afoul of (12) because it may proceed after
hardenfor a subject with unfrozen objects in its volume. - A library designed to work pre-lockdown with any eligible flavor of
hardenexcept throwing harden may fail to work post-lockdown with a throwing harden post-lockdown if the author failed to harden a prototype of a hardened instance. - Moving to use throwing harden would be a breaking change for some existing libraries and would need to be opted-in with a
lockdownoption. - Neither harden until intrinsics or harden until global are eligible post-lockdown because (12) they leave shared mutable objects in their wake. These are strictly pre-lockdown variants.
- Both harden until intrinsics and harden until global close over an extent that can get captured in a bundled version of
@endo/harden. This is fine for post-lockdown usage because the bundled version will be ignored. This may be fine pre-lockdown because hardening an instance won’t accidentally freeze something that might be shimmed, particularly not objects that hide behind method invocation, unless you consider the case of shimming an object exported by another module by pre-emptively importing it.
Tangentially, the regarding whether @endo/harden should prefer Object[Symbol.for('harden')] over globalThis.harden:
- Favor Object because
Objectwill always be frozen post-lockdown andglobalThisis not necessarily. This is a weakly preferable since Compartments are not generally co-tenant and co-tenant Compartments must freezeglobalThis. - Favor globalThis because this gives individual compartments the option of receiving from without or installing from within a specific alternative
globalThis.harden.
My tentative preference is:
sespost-lockdown provides assuring harden by defaultsespost-lockdown provides throwing harden if configured so folks can opt into early discovery of unhardened prototypes. (Recall that we can’t do warning harden post-lockdown.)@endo/hardenraces to provide a fake harden implementation@endo/hardenwith-C hardenedpresumeslockdownand throws upon initialization if it doesn’t findhardenin the environment@endo/hardenwith other-Cconditions to race to install stronger pre-lockdown harden like throwing harden or warning harden, maybe suppressing warnings for objects reachable from the volume of global or known shared intrinsics.