Skip to content

Decoupling of lockdown and Endo’s hardened modules #2983

@kriskowal

Description

@kriskowal

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

Option: Provide an @endo/harden that is a no-op by default with build-condition and lockdown-option opt-in for alternatives

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:

  1. 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,
  2. Suppose that we partition all valid programs into those that use a pre-lockdown harden and those that use a post-lockdown harden, and any application that tries to do both fails when they try to lockdown.
  3. To ensure A, suppose that co-tenant @endo/harden implementations race to install non-writable, non-configurable Object[Symbol.for('harden')] upon the first invocation of harden. Any instance of @endo/harden is eligible to win the race and determines the semantics (and holder of the memo), and the losers silently tolerate losing the race. Whereas, if lockdown loses the race, it throws an exception.
  4. 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.
  5. The @endo/harden module would fail upon first attempt to harden inside a Compartment in composition with a version of SES that does not install Object[Symbol.for('harden')] and where globalThis.harden was not endowed. No such versions of ses currently exist, as compartments receive harden by default. No future version of ses would break because it will introduce Object[Symbol.for('harden')] which cannot be denied by the creator of a Compartment. It is a shared intrinsic that cannot be omitted from a Compartment except under extreme duress.
  6. To ensure that @endo/harden works with versions of SES that predate the decision to install Object[Symbol.for('harden')], it will also sense (upon first invocation) that it lost the race if it instead finds globalThis.harden is installed, and will fall-through to that instead.
  7. There would be no versions of @endo/harden that predate the choice to install Object[Symbol.for('harden')], so no mutual compatibility concerns among versions of @endo/harden.
  8. Using the "exports" directive in the @endo/harden package.json, we can let the user of an application or the creator of a bundle elect a mode of harden that 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 like bundle-source -C shallow-harden
  9. 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 harden and use the one installed at globalThis.harden or Object[Symbol.for('harden')]. That would look like bundle-source -C lockded-down. So, bundles that migrate from assuming the existence of a post-lockdown globalThis.harden can migrate to use @endo/harden, become more portable, without incurring a significant bundle size increase.
  10. For every mode of @endo/harden, the mode variant only applies to the pre-lockdown behavior.
  11. Ideally, a library that is built to use @endo/harden does not need to separately test its behavior pre- and post-lockdown. If it works with pre-lockdown, it should not break post-lockdown.
  12. All post-lockdown harden options must guarantee that the transitive closure of both prototypes and properties are frozen, because calling harden should relieve the reviewer of any concern of reachable transitive mutability, even for code that might be run pre-lockdown.
  13. Regardless of whether harden closes 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 opportunistically harden.

The dimensions to the design of a harden implementation are:

  • pre vs post
  • harden extent the extent that harden freezes
  • throwing extent the extent in which harden will throw if it finds an unfrozen object
  • warning extent the extent in which harden will 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 lockdown because it hardens shared intrinsics in a way that would interfere with lockdown, and also can interfere with shims of anything captured by a hardened instance. It’s technically at peace with lockdown because (2) ensures that using harden before lockdown foreswears ever calling lockdown. 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 harden for a subject with unfrozen objects in its volume.
  • A library designed to work pre-lockdown with any eligible flavor of harden except 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 lockdown option.
  • 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 Object will always be frozen post-lockdown and globalThis is not necessarily. This is a weakly preferable since Compartments are not generally co-tenant and co-tenant Compartments must freeze globalThis.
  • 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:

  • ses post-lockdown provides assuring harden by default
  • ses post-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/harden races to provide a fake harden implementation
  • @endo/harden with -C hardened presumes lockdown and throws upon initialization if it doesn’t find harden in the environment
  • @endo/harden with other -C conditions 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions