Skip to content

feat(@endo/harden): Lightweight pre-lockdown hardened module support#2976

Closed
kriskowal wants to merge 8 commits intomasterfrom
kriskowal-endo-harden-lite
Closed

feat(@endo/harden): Lightweight pre-lockdown hardened module support#2976
kriskowal wants to merge 8 commits intomasterfrom
kriskowal-endo-harden-lite

Conversation

@kriskowal
Copy link
Member

@kriskowal kriskowal commented Oct 1, 2025

Refs: #2983
Refs: #1686
Refs: #2782

Description

This very small change introduces an @endo/harden package that provides a fake harden by default, but in a way that ensures that it is never used in composition with lockdown.
Description of the feature included in the PR documents.

While this avoids the need to eject the harden internals of ses into another package, along with the complications that entrained in #2782, we may elect to provide additional build conditions for @endo/harden that provide greater degrees of immutability to applications that opt-in, either for their own safety or for earlier detection of inadequate prototype hardening.

Security Considerations

Currently, hardened modules can only be used after lockdown. This change introduces a new era where some hardened modules can be used before lockdown if the application forswears ever using lockdown in the same realm. If a hardened module is initialized before lockdown in error, this approach ensures that the developer gets a useful diagnostic with a stack pointed at the offending module.

Scaling Considerations

Modules that adopt @endo/harden will be slightly larger. The overhead can be reduced considerably by using the bundle-source -C hardened condition, in which case the additional module will only be a few lines and any additional excess is in the package linkage.

Documentation Considerations

This new feature is documented in the ses NEWS and the new harden module’s README.

Testing Considerations

This module required multiple modules to exercise the various destructive behaviors of lockdown relative to when harden gets called.

Compatibility Considerations

Hardened modules were not previously usable before lockdown. This change commits us to a course where modules can be used before lockdown. Also, lockdown on previous hardened modules was already inadequate to the task of identifying gaps where the hardening of a module was not rigorous enough to the extent that prototypes were frozen hardened before the first instance was hardened. This is not a regression but does not create progress on that front. For progress toward ensuring rigorous hardening, I propose future changes that introduce new lockdown modes and @endo/harden build conditions that create the conditions for opt-in detection of inadequately hardened prototype chains.

The provision of harden.isFake is intended to compose well with other libraries that sense this property as an indication that they’re in a less-immutable environment.

Upgrade Considerations

This change creates some coordination risks between versions of harden captured in bundles with divergent behavior from host environments that provide harden. I believe all of the matrix of possible interactions is accounted for.

  • New bundles may entrain @endo/harden which will defer to globalThis.harden on platforms using older SES. There are some possible challenges with bundle size changing, but the changes should be minimal, and the build condition mitigation provided is not likely to make a big difference.
  • Old hardened modules will continue to use globalThis.harden directly.
  • New versions of SES will provide an additional Object[@harden] intrinsic that new code may come to rely upon. This new code will not be portable to older SES platforms. However, using a symbol and providing @endo/harden as a typed facade should discourage the creation of incompatible code.

@kriskowal kriskowal force-pushed the kriskowal-endo-harden-lite branch 4 times, most recently from 23d32db to ff78924 Compare October 1, 2025 02:01
@kriskowal kriskowal force-pushed the kriskowal-endo-harden-lite branch from ff78924 to 1032d2f Compare October 1, 2025 02:15
@kriskowal kriskowal mentioned this pull request Oct 1, 2025
Copy link
Member

@michaelfig michaelfig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! I was curious, so I took the time to review.

harden(myFunction);

export const MyConstructor = function MyConstructor() {};
harden(MyConstructor.prototype);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be off-base, but this line seems a little misleading, IIUC the latest harden design. Under HardenedJS, isn't it supposed to at the very least recurse over own properties (i.e. Reflect.ownKeys(obj)), but not prototypes (i.e. Reflect.getPrototypeOf(obj))?

With that in mind, harden(MyConstructor.prototype) seems completely redundant, since .prototype is an own property of MyConstructor, since it will be hardened upon calling harden(MyConstructor).

Reading further, the proposed pre-lockdown harden is a no-op. If it's never recursive (not even under HardenedJS), it just seems like an attractive nuisance.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right that this is not a good example. I may need to appeal to @mhofman for a good example of a case where a defensively hardened module accidentally leaves something mutable exposed until the first instance is hardened. I’m likely confused and it’s possible that such a case is only possible with a speculatively different combination of alternative harden behaviors.

Comment on lines +14 to +15
- Adds `Object[Symbol.for('harden')]`, a variation on `globalThis.harden` that
cannot be denied by endowment of an alternative `harden` to compartments.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhhh. Now it makes sense! You may want to call this out in the packages/harden/README.md.

@kriskowal kriskowal force-pushed the kriskowal-endo-harden-lite branch 4 times, most recently from 279bc3a to e6c94e6 Compare October 3, 2025 04:17
@kriskowal kriskowal force-pushed the kriskowal-endo-harden-lite branch 7 times, most recently from 9fa1896 to c6f88ff Compare October 4, 2025 02:26
const harden = o => o;
harden.isFake = true;
harden.lockdownError = new Error(
'Cannot lockdown (repairIntrinsics) because @endo/harden used before lockdown on this stack',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "on this stack" mean?

Copy link
Contributor

@erights erights left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on a partial review of #2978 , what I want from this PR is:

  • Have the normal noses testing config not redact, so ALMOST ALL the changes to golden error message tests can revert.
  • Abstract harden.isFake || Object.isFrozen(...) into a reusable abstraction, and use it.

Together, these would restore the brevity and readability of most existing tests.

@kriskowal kriskowal force-pushed the kriskowal-endo-harden-lite branch from c6f88ff to edf0502 Compare October 7, 2025 00:27
@kriskowal
Copy link
Member Author

Now favoring third approach #3008

@kriskowal kriskowal closed this Nov 7, 2025
@kriskowal
Copy link
Member Author

Notably, the new approach relies on #3005 so tests using SES-AVA do not observe different error messages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants