From 0fa26f17b3e944016891123bd734cbc462e6c2f8 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 30 Sep 2025 17:57:56 -0700 Subject: [PATCH 01/22] feat(ses): Sense @endo/harden usage before lockdown --- packages/ses/NEWS.md | 9 +++++++++ packages/ses/src/lockdown.js | 25 +++++++++++++++++++++++++ packages/ses/src/permits.js | 5 +++++ 3 files changed, 39 insertions(+) diff --git a/packages/ses/NEWS.md b/packages/ses/NEWS.md index d3170f3606..56a02e7c4c 100644 --- a/packages/ses/NEWS.md +++ b/packages/ses/NEWS.md @@ -4,6 +4,15 @@ User-visible changes in `ses` - Adds `assert.makeError` and deprecates `assert.error` as an alias, matching the API already exported from `@endo/errors`. +- The `lockdown` and `repairIntrinsics` functions will now sense if a + "hardened module" (one using the new `@endo/harden`) was initialized before + `lockdown`, causing `lockdown` to fail with an informative error indicating + that hardened modules were not properly initialized because they executed + before lockdown. + They sense this condition because `@endo/harden` will install its own + `Object[Symbol.for('harden')]` if `harden` gets used before `lockdown`. +- Adds `Object[Symbol.for('harden')]`, a variation on `globalThis.harden` that + cannot be denied by endowment of an alternative `harden` to compartments. # v1.13.0 (2025-06-02) diff --git a/packages/ses/src/lockdown.js b/packages/ses/src/lockdown.js index 42bfcb18fc..9b378d4c71 100644 --- a/packages/ses/src/lockdown.js +++ b/packages/ses/src/lockdown.js @@ -25,6 +25,7 @@ import { is, ownKeys, stringSplit, + symbolFor, noEvalEvaluate, getOwnPropertyNames, getPrototypeOf, @@ -363,6 +364,30 @@ export const repairIntrinsics = (options = {}) => { const intrinsics = finalIntrinsics(); + // Install Object[@harden] or abort. + const symbolForHarden = symbolFor('harden'); + const priorHarden = intrinsics.Object[symbolForHarden]; + if (priorHarden) { + // By convention, if a module like @endo/harden gets used before lockdown, + // it will install itself as a non-configurable, non-writable property over + // Object[@harden] so that versions of SES predating the introduction of + // Object[@harden] will fail to lockdown because they cannot remove an + // unknown intrinsic. + // All newer versions explicitly check for Object[@harden] (here). + // The @endo/harden implementation additionally captures a stack trace + // where harden was first used to assist developers in tracking down the + // hardened module that was initialized before lockdown. + if (priorHarden.lockdownError) { + throw priorHarden.lockdownError; + } + // And in the event a library installs Object[@harden] without leaving a + // hint, we fall back to a generic lockdown error. + throw new TypeError( + 'Cannot lockdown (repairIntrinsics) if a prior harden implementation has been used and installed. Check for libraries using @endo/harden before lockdown.', + ); + } + intrinsics.Object[symbolForHarden] = tamedHarden; + const hostIntrinsics = { __proto__: null }; // The Node.js Buffer is a derived class of Uint8Array, and as such is often diff --git a/packages/ses/src/permits.js b/packages/ses/src/permits.js index 929baaec61..8680b60666 100644 --- a/packages/ses/src/permits.js +++ b/packages/ses/src/permits.js @@ -507,6 +507,11 @@ export const permitted = { seal: fn, setPrototypeOf: fn, values: fn, + 'RegisteredSymbol(harden)': { + ...fn, + // Installed with hardenTaming: 'unsafe' + isFake: 'boolean', + }, // https://github.com/tc39/proposal-accessible-object-hasownproperty hasOwn: fn, // https://github.com/tc39/proposal-array-grouping From cc39fc14e8ecec072464bb5e38eecfa2406c6922 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 30 Sep 2025 16:10:26 -0700 Subject: [PATCH 02/22] chore(harden): Create from scaffold --- packages/harden/CHANGELOG.md | 6 + packages/harden/LICENSE | 201 ++++++++++++++++++++++++++++ packages/harden/NEWS.md | 1 + packages/harden/README.md | 3 + packages/harden/SECURITY.md | 38 ++++++ packages/harden/index.js | 0 packages/harden/package.json | 71 ++++++++++ packages/harden/test/index.test.js | 5 + packages/harden/tsconfig.build.json | 9 ++ packages/harden/tsconfig.json | 9 ++ 10 files changed, 343 insertions(+) create mode 100644 packages/harden/CHANGELOG.md create mode 100644 packages/harden/LICENSE create mode 100644 packages/harden/NEWS.md create mode 100644 packages/harden/README.md create mode 100644 packages/harden/SECURITY.md create mode 100644 packages/harden/index.js create mode 100644 packages/harden/package.json create mode 100644 packages/harden/test/index.test.js create mode 100644 packages/harden/tsconfig.build.json create mode 100644 packages/harden/tsconfig.json diff --git a/packages/harden/CHANGELOG.md b/packages/harden/CHANGELOG.md new file mode 100644 index 0000000000..8ff367903e --- /dev/null +++ b/packages/harden/CHANGELOG.md @@ -0,0 +1,6 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + + diff --git a/packages/harden/LICENSE b/packages/harden/LICENSE new file mode 100644 index 0000000000..880aceeb4e --- /dev/null +++ b/packages/harden/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Endo Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/harden/NEWS.md b/packages/harden/NEWS.md new file mode 100644 index 0000000000..4c3268a7f8 --- /dev/null +++ b/packages/harden/NEWS.md @@ -0,0 +1 @@ +User-visible changes in `@endo/harden`: diff --git a/packages/harden/README.md b/packages/harden/README.md new file mode 100644 index 0000000000..303683fd0e --- /dev/null +++ b/packages/harden/README.md @@ -0,0 +1,3 @@ +# harden + +This `@endo/harden` package is a skeleton package. diff --git a/packages/harden/SECURITY.md b/packages/harden/SECURITY.md new file mode 100644 index 0000000000..9dbbb79534 --- /dev/null +++ b/packages/harden/SECURITY.md @@ -0,0 +1,38 @@ +# Security Policy + +## Supported Versions + +The SES package and associated Endo packages are still undergoing development and security review, and all +users are encouraged to use the latest version available. Security fixes will +be made for the most recent branch only. + +## Coordinated Vulnerability Disclosure of Security Bugs + +SES stands for fearless cooperation, and strong security requires strong collaboration with security researchers. If you believe that you have found a security sensitive bug that should not be disclosed until a fix has been made available, we encourage you to report it. To report a bug in HardenedJS, you have several options that include: + +* Reporting the issue to the [Agoric HackerOne vulnerability rewards program](https://hackerone.com/agoric). + +* Sending an email to security at (@) agoric.com., encrypted or unencrypted. To encrypt, please use @Warner’s personal GPG key [A476E2E6 11880C98 5B3C3A39 0386E81B 11CAA07A](http://www.lothar.com/warner-gpg.html) . + +* Sending a message on Keybase to `@agoric_security`, or sharing code and other log files via Keybase’s encrypted file system. ((_keybase_private/agoric_security,$YOURNAME). + +* It is important to be able to provide steps that reproduce the issue and demonstrate its impact with a Proof of Concept example in an initial bug report. Before reporting a bug, a reporter may want to have another trusted individual reproduce the issue. + +* A bug reporter can expect acknowledgment of a potential vulnerability reported through [security@agoric.com](mailto:security@agoric.com) within one business day of submitting a report. If an acknowledgement of an issue is not received within this time frame, especially during a weekend or holiday period, please reach out again. Any issues reported to the HackerOne program will be acknowledged within the time frames posted on the program page. + * The bug triage team and Agoric code maintainers are primarily located in the San Francisco Bay Area with business hours in [Pacific Time](https://www.timeanddate.com/worldclock/usa/san-francisco) . + +* For the safety and security of those who depend on the code, bug reporters should avoid publicly sharing the details of a security bug on Twitter, Discord, Telegram, or in public Github issues during the coordination process. + +* Once a vulnerability report has been received and triaged: + * Agoric code maintainers will confirm whether it is valid, and will provide updates to the reporter on validity of the report. + * It may take up to 72 hours for an issue to be validated, especially if reported during holidays or on weekends. + +* When the Agoric team has verified an issue, remediation steps and patch release timeline information will be shared with the reporter. + * Complexity, severity, impact, and likelihood of exploitation are all vital factors that determine the amount of time required to remediate an issue and distribute a software patch. + * If an issue is Critical or High Severity, Agoric code maintainers will release a security advisory to notify impacted parties to prepare for an emergency patch. + * While the current industry standard for vulnerability coordination resolution is 90 days, Agoric code maintainers will strive to release a patch as quickly as possible. + +When a bug patch is included in a software release, the Agoric code maintainers will: + * Confirm the version and date of the software release with the reporter. + * Provide information about the security issue that the software release resolves. + * Credit the bug reporter for discovery by adding thanks in release notes, securing a CVE designation, or adding the researcher’s name to a Hall of Fame. diff --git a/packages/harden/index.js b/packages/harden/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/harden/package.json b/packages/harden/package.json new file mode 100644 index 0000000000..b68c946de6 --- /dev/null +++ b/packages/harden/package.json @@ -0,0 +1,71 @@ +{ + "name": "@endo/harden", + "version": "1.0.0", + "description": "For hardened libraries, regardless of hardened environments", + "keywords": [], + "author": "Endo contributors", + "license": "Apache-2.0", + "homepage": "https://github.com/endojs/endo/tree/master/packages/harden#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/endojs/endo.git", + "directory": "packages/harden" + }, + "bugs": { + "url": "https://github.com/endojs/endo/issues" + }, + "type": "module", + "main": "./index.js", + "module": "./index.js", + "exports": { + ".": "./index.js", + "./package.json": "./package.json" + }, + "scripts": { + "build": "exit 0", + "lint": "yarn lint:types && yarn lint:eslint", + "lint-fix": "yarn lint:eslint --fix && yarn lint:types", + "lint:eslint": "eslint '**/*.js'", + "lint:types": "tsc", + "postpack": "git clean -fX \"*.d.ts*\" \"*.d.cts*\" \"*.d.mts*\" \"*.tsbuildinfo\"", + "prepack": "tsc --build tsconfig.build.json", + "test": "ava", + "test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js", + "test:xs": "exit 0" + }, + "dependencies": {}, + "devDependencies": { + "@endo/lockdown": "workspace:^", + "@endo/ses-ava": "workspace:^", + "ava": "catalog:dev", + "c8": "catalog:dev", + "eslint": "catalog:dev", + "tsd": "catalog:dev", + "typescript": "~5.9.2" + }, + "files": [ + "./*.d.ts", + "./*.js", + "./*.map", + "LICENSE*", + "SECURITY*", + "dist", + "lib", + "src", + "tools" + ], + "publishConfig": { + "access": "public" + }, + "eslintConfig": { + "extends": [ + "plugin:@endo/internal" + ] + }, + "ava": { + "files": [ + "test/**/*.test.*" + ], + "timeout": "2m" + } +} diff --git a/packages/harden/test/index.test.js b/packages/harden/test/index.test.js new file mode 100644 index 0000000000..bf5a26862c --- /dev/null +++ b/packages/harden/test/index.test.js @@ -0,0 +1,5 @@ +import test from '@endo/ses-ava/prepare-endo.js'; + +test('placeholder', async t => { + t.fail('TODO: add tests'); +}); diff --git a/packages/harden/tsconfig.build.json b/packages/harden/tsconfig.build.json new file mode 100644 index 0000000000..9cd34b05a3 --- /dev/null +++ b/packages/harden/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": [ + "./tsconfig.json", + "../../tsconfig-build-options.json" + ], + "exclude": [ + "test/" + ] +} diff --git a/packages/harden/tsconfig.json b/packages/harden/tsconfig.json new file mode 100644 index 0000000000..1f01375650 --- /dev/null +++ b/packages/harden/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.eslint-base.json", + "include": [ + "*.js", + "*.ts", + "src", + "test" + ] +} From c385372abed91efa7706dade30ff812a28754e4a Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 07:10:07 -0700 Subject: [PATCH 03/22] refactor(harden): Remove scaffold dross --- packages/harden/index.js | 0 packages/harden/package.json | 6 ++---- packages/harden/test/index.test.js | 5 ----- 3 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 packages/harden/index.js delete mode 100644 packages/harden/test/index.test.js diff --git a/packages/harden/index.js b/packages/harden/index.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/harden/package.json b/packages/harden/package.json index b68c946de6..0028ceeb89 100644 --- a/packages/harden/package.json +++ b/packages/harden/package.json @@ -30,16 +30,14 @@ "postpack": "git clean -fX \"*.d.ts*\" \"*.d.cts*\" \"*.d.mts*\" \"*.tsbuildinfo\"", "prepack": "tsc --build tsconfig.build.json", "test": "ava", - "test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js", + "test:c8": "c8 ${C8_OPTIONS:-} ava", "test:xs": "exit 0" }, - "dependencies": {}, "devDependencies": { - "@endo/lockdown": "workspace:^", - "@endo/ses-ava": "workspace:^", "ava": "catalog:dev", "c8": "catalog:dev", "eslint": "catalog:dev", + "ses": "workspace:^", "tsd": "catalog:dev", "typescript": "~5.9.2" }, diff --git a/packages/harden/test/index.test.js b/packages/harden/test/index.test.js deleted file mode 100644 index bf5a26862c..0000000000 --- a/packages/harden/test/index.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import test from '@endo/ses-ava/prepare-endo.js'; - -test('placeholder', async t => { - t.fail('TODO: add tests'); -}); From 256624f7e77077a4ecc32a801f56580478fbeb23 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 9 Oct 2025 19:11:52 -0700 Subject: [PATCH 04/22] refactor(harden): Duplicate ses make-hardener.js without modification --- packages/harden/make-hardener.js | 276 +++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 packages/harden/make-hardener.js diff --git a/packages/harden/make-hardener.js b/packages/harden/make-hardener.js new file mode 100644 index 0000000000..80e55b911b --- /dev/null +++ b/packages/harden/make-hardener.js @@ -0,0 +1,276 @@ + +// Adapted from SES/Caja - Copyright (C) 2011 Google Inc. +// Copyright (C) 2018 Agoric + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// based upon: +// https://github.com/google/caja/blob/master/src/com/google/caja/ses/startSES.js +// https://github.com/google/caja/blob/master/src/com/google/caja/ses/repairES5.js +// then copied from proposal-frozen-realms deep-freeze.js +// then copied from SES/src/bundle/deepFreeze.js + +// @ts-check + +import { + Set, + String, + TypeError, + WeakSet, + globalThis, + apply, + arrayForEach, + defineProperty, + freeze, + getOwnPropertyDescriptor, + getOwnPropertyDescriptors, + getPrototypeOf, + isInteger, + isPrimitive, + hasOwn, + ownKeys, + preventExtensions, + setAdd, + setForEach, + setHas, + toStringTagSymbol, + typedArrayPrototype, + weaksetAdd, + weaksetHas, + FERAL_STACK_GETTER, + FERAL_STACK_SETTER, + isError, +} from './commons.js'; +import { assert } from './error/assert.js'; + +/** + * @import {Harden} from '../types.js' + */ + +// Obtain the string tag accessor of of TypedArray so we can indirectly use the +// TypedArray brand check it employs. +const typedArrayToStringTag = getOwnPropertyDescriptor( + typedArrayPrototype, + toStringTagSymbol, +); +assert(typedArrayToStringTag); +const getTypedArrayToStringTag = typedArrayToStringTag.get; +assert(getTypedArrayToStringTag); + +// Exported for tests. +/** + * Duplicates packages/marshal/src/helpers/passStyle-helpers.js to avoid a dependency. + * + * @param {unknown} object + */ +export const isTypedArray = object => { + // The object must pass a brand check or toStringTag will return undefined. + const tag = apply(getTypedArrayToStringTag, object, []); + return tag !== undefined; +}; + +/** + * Tests if a property key is an integer-valued canonical numeric index. + * https://tc39.es/ecma262/#sec-canonicalnumericindexstring + * + * @param {string | symbol} propertyKey + */ +const isCanonicalIntegerIndexString = propertyKey => { + const n = +String(propertyKey); + return isInteger(n) && String(n) === propertyKey; +}; + +/** + * @template T + * @param {ArrayLike} array + */ +const freezeTypedArray = array => { + preventExtensions(array); + + // Downgrade writable expandos to readonly, even if non-configurable. + // We get each descriptor individually rather than using + // getOwnPropertyDescriptors in order to fail safe when encountering + // an obscure GraalJS issue where getOwnPropertyDescriptor returns + // undefined for a property that does exist. + arrayForEach(ownKeys(array), (/** @type {string | symbol} */ name) => { + const desc = getOwnPropertyDescriptor(array, name); + assert(desc); + // TypedArrays are integer-indexed exotic objects, which define special + // treatment for property names in canonical numeric form: + // integers in range are permanently writable and non-configurable. + // https://tc39.es/ecma262/#sec-integer-indexed-exotic-objects + // + // This is analogous to the data of a hardened Map or Set, + // so we carve out this exceptional behavior but make all other + // properties non-configurable. + if (!isCanonicalIntegerIndexString(name)) { + defineProperty(array, name, { + ...desc, + writable: false, + configurable: false, + }); + } + }); +}; + +/** + * Create a `harden` function. + * + * @returns {Harden} + */ +export const makeHardener = () => { + // Use a native hardener if possible. + if (typeof globalThis.harden === 'function') { + const safeHarden = globalThis.harden; + return safeHarden; + } + + const hardened = new WeakSet(); + + const { harden } = { + /** + * @template T + * @param {T} root + * @returns {T} + */ + harden(root) { + const toFreeze = new Set(); + + // If val is something we should be freezing but aren't yet, + // add it to toFreeze. + /** + * @param {any} val + */ + function enqueue(val) { + if (isPrimitive(val)) { + // ignore primitives + return; + } + const type = typeof val; + if (type !== 'object' && type !== 'function') { + // future proof: break until someone figures out what it should do + throw TypeError(`Unexpected typeof: ${type}`); + } + if (weaksetHas(hardened, val) || setHas(toFreeze, val)) { + // Ignore if this is an exit, or we've already visited it + return; + } + // console.warn(`adding ${val} to toFreeze`, val); + setAdd(toFreeze, val); + } + + /** + * @param {any} obj + */ + const baseFreezeAndTraverse = obj => { + // Now freeze the object to ensure reactive + // objects such as proxies won't add properties + // during traversal, before they get frozen. + + // Object are verified before being enqueued, + // therefore this is a valid candidate. + // Throws if this fails (strict mode). + // Also throws if the object is an ArrayBuffer or any TypedArray. + if (isTypedArray(obj)) { + freezeTypedArray(obj); + } else { + freeze(obj); + } + + // we rely upon certain commitments of Object.freeze and proxies here + + // get stable/immutable outbound links before a Proxy has a chance to do + // something sneaky. + const descs = getOwnPropertyDescriptors(obj); + const proto = getPrototypeOf(obj); + enqueue(proto); + + arrayForEach(ownKeys(descs), (/** @type {string | symbol} */ name) => { + // The 'name' may be a symbol, and TypeScript doesn't like us to + // index arbitrary symbols on objects, so we pretend they're just + // strings. + const desc = descs[/** @type {string} */ (name)]; + // getOwnPropertyDescriptors is guaranteed to return well-formed + // descriptors, but they still inherit from Object.prototype. If + // someone has poisoned Object.prototype to add 'value' or 'get' + // properties, then a simple 'if ("value" in desc)' or 'desc.value' + // test could be confused. We use hasOwnProperty to be sure about + // whether 'value' is present or not, which tells us for sure that + // this is a data property. + if (hasOwn(desc, 'value')) { + enqueue(desc.value); + } else { + enqueue(desc.get); + enqueue(desc.set); + } + }); + }; + + const freezeAndTraverse = + FERAL_STACK_GETTER === undefined && FERAL_STACK_SETTER === undefined + ? // On platforms without v8's error own stack accessor problem, + // don't pay for any extra overhead. + baseFreezeAndTraverse + : obj => { + if (isError(obj)) { + // Only pay the overhead if it first passes this cheap isError + // check. Otherwise, it will be unrepaired, but won't be judged + // to be a passable error anyway, so will not be unsafe. + const stackDesc = getOwnPropertyDescriptor(obj, 'stack'); + if ( + stackDesc && + stackDesc.get === FERAL_STACK_GETTER && + stackDesc.configurable + ) { + // Can only repair if it is configurable. Otherwise, leave + // unrepaired, in which case it will not be judged passable, + // avoiding a safety problem. + defineProperty(obj, 'stack', { + // NOTE: Calls getter during harden, which seems dangerous. + // But we're only calling the problematic getter whose + // hazards we think we understand. + // @ts-expect-error TS should know FERAL_STACK_GETTER + // cannot be `undefined` here. + // See https://github.com/endojs/endo/pull/2232#discussion_r1575179471 + value: apply(FERAL_STACK_GETTER, obj, []), + }); + } + } + return baseFreezeAndTraverse(obj); + }; + + const dequeue = () => { + // New values added before forEach() has finished will be visited. + setForEach(toFreeze, freezeAndTraverse); + }; + + /** @param {any} value */ + const markHardened = value => { + weaksetAdd(hardened, value); + }; + + const commit = () => { + setForEach(toFreeze, markHardened); + }; + + enqueue(root); + dequeue(); + // console.warn("toFreeze set:", toFreeze); + commit(); + + return root; + }, + }; + + return harden; +}; From 33c8cf6e6eb4b1bc84f6ad6e176cc51863bd0aa5 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 9 Oct 2025 19:13:27 -0700 Subject: [PATCH 05/22] refactor(harden): Relive makeHardener of assert dependency --- packages/harden/make-hardener.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/harden/make-hardener.js b/packages/harden/make-hardener.js index 80e55b911b..18f8f6d918 100644 --- a/packages/harden/make-hardener.js +++ b/packages/harden/make-hardener.js @@ -51,7 +51,13 @@ import { FERAL_STACK_SETTER, isError, } from './commons.js'; -import { assert } from './error/assert.js'; + +/** @type {(condition: any) => asserts condition} */ +const assert = condition => { + if (!condition) { + throw new TypeError('assertion failed'); + } +}; /** * @import {Harden} from '../types.js' From c3fc16dc1e3bfe30371a80ef2defda0d08ec617a Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 9 Oct 2025 19:14:10 -0700 Subject: [PATCH 06/22] refactor(harden): Make traversePrototypes option of makeHardener --- packages/harden/make-hardener.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/harden/make-hardener.js b/packages/harden/make-hardener.js index 18f8f6d918..d586919008 100644 --- a/packages/harden/make-hardener.js +++ b/packages/harden/make-hardener.js @@ -132,15 +132,11 @@ const freezeTypedArray = array => { /** * Create a `harden` function. * + * @param {object} [args] + * @param {boolean} [args.traversePrototypes] * @returns {Harden} */ -export const makeHardener = () => { - // Use a native hardener if possible. - if (typeof globalThis.harden === 'function') { - const safeHarden = globalThis.harden; - return safeHarden; - } - +export const makeHardener = ({ traversePrototypes = false } = {}) => { const hardened = new WeakSet(); const { harden } = { @@ -198,8 +194,10 @@ export const makeHardener = () => { // get stable/immutable outbound links before a Proxy has a chance to do // something sneaky. const descs = getOwnPropertyDescriptors(obj); - const proto = getPrototypeOf(obj); - enqueue(proto); + if (traversePrototypes) { + const proto = getPrototypeOf(obj); + enqueue(proto); + } arrayForEach(ownKeys(descs), (/** @type {string | symbol} */ name) => { // The 'name' may be a symbol, and TypeScript doesn't like us to From 58713a8397e5f420bce7d020d7e5cb260bb6887d Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 9 Oct 2025 19:22:45 -0700 Subject: [PATCH 07/22] refactor(harden): Inline ses commons.js into make-hardener.js without modification --- packages/harden/make-hardener.js | 441 +++++++++++++++++++++++++++++-- 1 file changed, 420 insertions(+), 21 deletions(-) diff --git a/packages/harden/make-hardener.js b/packages/harden/make-hardener.js index d586919008..5ce7e0b5b8 100644 --- a/packages/harden/make-hardener.js +++ b/packages/harden/make-hardener.js @@ -22,35 +22,434 @@ // @ts-check -import { +// We cannot use globalThis as the local name since it would capture the +// lexical name. +const universalThis = globalThis; +export { universalThis as globalThis }; + +export const { + Array, + ArrayBuffer, + Date, + FinalizationRegistry, + Float32Array, + JSON, + Map, + Math, + Number, + Object, + Promise, + Proxy, + Reflect, + RegExp: FERAL_REG_EXP, Set, String, - TypeError, + Symbol, + Uint8Array, + WeakMap, WeakSet, - globalThis, - apply, - arrayForEach, - defineProperty, +} = globalThis; + +export const { + // The feral Error constructor is safe for internal use, but must not be + // revealed to post-lockdown code in any compartment including the start + // compartment since in V8 at least it bears stack inspection capabilities. + Error: FERAL_ERROR, + RangeError, + ReferenceError, + SyntaxError, + TypeError, + AggregateError, +} = globalThis; + +export const { + assign, + create, + defineProperties, + entries, freeze, getOwnPropertyDescriptor, getOwnPropertyDescriptors, + getOwnPropertyNames, getPrototypeOf, - isInteger, - isPrimitive, - hasOwn, - ownKeys, + is, + isFrozen, + isSealed, + isExtensible, + keys, + prototype: objectPrototype, + seal, preventExtensions, - setAdd, - setForEach, - setHas, - toStringTagSymbol, - typedArrayPrototype, - weaksetAdd, - weaksetHas, - FERAL_STACK_GETTER, - FERAL_STACK_SETTER, - isError, -} from './commons.js'; + setPrototypeOf, + values, + fromEntries, +} = Object; + +export const { + species: speciesSymbol, + toStringTag: toStringTagSymbol, + iterator: iteratorSymbol, + matchAll: matchAllSymbol, + unscopables: unscopablesSymbol, + keyFor: symbolKeyFor, + for: symbolFor, +} = Symbol; + +export const { isInteger } = Number; + +export const { stringify: stringifyJson } = JSON; + +// 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; +}; + +export const { + apply, + construct, + get: reflectGet, + getOwnPropertyDescriptor: reflectGetOwnPropertyDescriptor, + has: reflectHas, + isExtensible: reflectIsExtensible, + ownKeys, + preventExtensions: reflectPreventExtensions, + set: reflectSet, +} = Reflect; + +export const { isArray, prototype: arrayPrototype } = Array; +export const { prototype: arrayBufferPrototype } = ArrayBuffer; +export const { prototype: mapPrototype } = Map; +export const { revocable: proxyRevocable } = Proxy; +export const { prototype: regexpPrototype } = RegExp; +export const { prototype: setPrototype } = Set; +export const { prototype: stringPrototype } = String; +export const { prototype: weakmapPrototype } = WeakMap; +export const { prototype: weaksetPrototype } = WeakSet; +export const { prototype: functionPrototype } = Function; +export const { prototype: promisePrototype } = Promise; +export const { prototype: generatorPrototype } = getPrototypeOf( + // eslint-disable-next-line no-empty-function, func-names + function* () {}, +); +export const iteratorPrototype = getPrototypeOf( + // eslint-disable-next-line @endo/no-polymorphic-call + getPrototypeOf(arrayPrototype.values()), +); + +export const typedArrayPrototype = getPrototypeOf(Uint8Array.prototype); + +const { bind } = functionPrototype; + +/** + * uncurryThis() + * Equivalent of: fn => (thisArg, ...args) => apply(fn, thisArg, args) + * + * See those reference for a complete explanation: + * http://wiki.ecmascript.org/doku.php?id=conventions:safe_meta_programming + * which only lives at + * http://web.archive.org/web/20160805225710/http://wiki.ecmascript.org/doku.php?id=conventions:safe_meta_programming + * + * @type { any>(fn: F) => ((thisArg: ThisParameterType, ...args: Parameters) => ReturnType)} + */ +export const uncurryThis = bind.bind(bind.call); // eslint-disable-line @endo/no-polymorphic-call + +// See https://github.com/endojs/endo/issues/2930 +if (!('hasOwn' in Object)) { + const ObjectPrototypeHasOwnProperty = objectPrototype.hasOwnProperty; + const hasOwnShim = (obj, key) => { + if (obj === undefined || obj === null) { + // We need to add this extra test because of differences in + // the order in which `hasOwn` vs `hasOwnProperty` validates + // arguments. + throw TypeError('Cannot convert undefined or null to object'); + } + return apply(ObjectPrototypeHasOwnProperty, obj, [key]); + }; + defineProperty(Object, 'hasOwn', { + value: hasOwnShim, + writable: true, + enumerable: false, + configurable: true, + }); +} + +export const { hasOwn } = Object; + +/** + * @deprecated Use `hasOwn` instead + */ +export const objectHasOwnProperty = hasOwn; +// +export const arrayFilter = uncurryThis(arrayPrototype.filter); +export const arrayForEach = uncurryThis(arrayPrototype.forEach); +export const arrayIncludes = uncurryThis(arrayPrototype.includes); +export const arrayJoin = uncurryThis(arrayPrototype.join); +/** @type {(thisArg: 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 {(thisArg: T[], ...items: T[]) => number} */ +export const arrayPush = uncurryThis(arrayPrototype.push); +export const arraySlice = uncurryThis(arrayPrototype.slice); +export const arraySome = uncurryThis(arrayPrototype.some); +export const arraySort = uncurryThis(arrayPrototype.sort); +export const iterateArray = uncurryThis(arrayPrototype[iteratorSymbol]); +// +export const arrayBufferSlice = uncurryThis(arrayBufferPrototype.slice); +/** @type {(b: ArrayBuffer) => number} */ +export const arrayBufferGetByteLength = uncurryThis( + // @ts-expect-error we know it is there on all conforming platforms + getOwnPropertyDescriptor(arrayBufferPrototype, 'byteLength').get, +); +// +export const typedArraySet = uncurryThis(typedArrayPrototype.set); +// +export const mapSet = uncurryThis(mapPrototype.set); +export const mapGet = uncurryThis(mapPrototype.get); +export const mapHas = uncurryThis(mapPrototype.has); +export const mapDelete = uncurryThis(mapPrototype.delete); +export const mapEntries = uncurryThis(mapPrototype.entries); +export const iterateMap = uncurryThis(mapPrototype[iteratorSymbol]); +// +export const setAdd = uncurryThis(setPrototype.add); +export const setDelete = uncurryThis(setPrototype.delete); +export const setForEach = uncurryThis(setPrototype.forEach); +export const setHas = uncurryThis(setPrototype.has); +export const iterateSet = uncurryThis(setPrototype[iteratorSymbol]); +// +export const regexpTest = uncurryThis(regexpPrototype.test); +export const regexpExec = uncurryThis(regexpPrototype.exec); +export const matchAllRegExp = uncurryThis(regexpPrototype[matchAllSymbol]); +// +export const stringEndsWith = uncurryThis(stringPrototype.endsWith); +export const stringIncludes = uncurryThis(stringPrototype.includes); +export const stringIndexOf = uncurryThis(stringPrototype.indexOf); +export const stringMatch = uncurryThis(stringPrototype.match); +export const generatorNext = uncurryThis(generatorPrototype.next); +export const generatorThrow = uncurryThis(generatorPrototype.throw); + +/** + * @type { & + * ((thisArg: string, searchValue: { [Symbol.replace](string: string, replaceValue: string): string; }, replaceValue: string) => string) & + * ((thisArg: 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) +); +export const stringSearch = uncurryThis(stringPrototype.search); +export const stringSlice = uncurryThis(stringPrototype.slice); +export const stringSplit = + /** @type {(thisArg: string, splitter: string | RegExp | { [Symbol.split](string: string, limit?: number): string[]; }, limit?: number) => string[]} */ ( + uncurryThis(stringPrototype.split) + ); +export const stringStartsWith = uncurryThis(stringPrototype.startsWith); +export const iterateString = uncurryThis(stringPrototype[iteratorSymbol]); +// +export const weakmapDelete = uncurryThis(weakmapPrototype.delete); +/** @type {(thisArg: WeakMap, ...args: Parameters['get']>) => ReturnType['get']>} */ +export const weakmapGet = uncurryThis(weakmapPrototype.get); +export const weakmapHas = uncurryThis(weakmapPrototype.has); +export const weakmapSet = uncurryThis(weakmapPrototype.set); +// +export const weaksetAdd = uncurryThis(weaksetPrototype.add); +export const weaksetHas = uncurryThis(weaksetPrototype.has); +// +export const functionToString = uncurryThis(functionPrototype.toString); +export const functionBind = uncurryThis(bind); +// +const { all } = Promise; +export const promiseAll = promises => apply(all, Promise, [promises]); +export const promiseCatch = uncurryThis(promisePrototype.catch); +/** @type {(thisArg: T, onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null) => Promise} */ +export const promiseThen = /** @type {any} */ ( + uncurryThis(promisePrototype.then) +); +// +export const finalizationRegistryRegister = + FinalizationRegistry && uncurryThis(FinalizationRegistry.prototype.register); +export const finalizationRegistryUnregister = + FinalizationRegistry && + uncurryThis(FinalizationRegistry.prototype.unregister); + +/** + * getConstructorOf() + * Return the constructor from an instance. + * + * @param {Function} fn + */ +export const getConstructorOf = fn => + reflectGet(getPrototypeOf(fn), 'constructor'); + +/** + * TODO Consolidate with `isPrimitive` that's currently in `@endo/pass-style`. + * Layering constraints make this tricky, which is why we haven't yet figured + * out how to do this. + * + * @type {(val: unknown) => val is (undefined + * | null + * | boolean + * | number + * | bigint + * | string + * | symbol)} + */ +export const isPrimitive = val => + !val || (typeof val !== 'object' && typeof val !== 'function'); + +/** + * isError tests whether an object inherits from the intrinsic + * `Error.prototype`. + * We capture the original error constructor as FERAL_ERROR to provide a clear + * signal for reviewers that we are handling an object with excess authority, + * like stack trace inspection, that we are carefully hiding from client code. + * Checking instanceof happens to be safe, but to avoid uttering FERAL_ERROR + * for such a trivial case outside commons.js, we provide a utility function. + * + * @param {any} value + */ +export const isError = value => value instanceof FERAL_ERROR; + +/** + * @template T + * @param {T} x + */ +export const identity = x => x; + +// The original unsafe untamed eval function, which must not escape. +// Sample at module initialization time, which is before lockdown can +// repair it. Use it only to build powerless abstractions. +// eslint-disable-next-line no-eval +export const FERAL_EVAL = eval; + +// The original unsafe untamed Function constructor, which must not escape. +// Sample at module initialization time, which is before lockdown can +// repair it. Use it only to build powerless abstractions. +export const FERAL_FUNCTION = Function; + +export const noEvalEvaluate = () => { + // See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_NO_EVAL.md + throw TypeError('Cannot eval with evalTaming set to "no-eval" (SES_NO_EVAL)'); +}; + +// ////////////////// FERAL_STACK_GETTER FERAL_STACK_SETTER //////////////////// + +const er1StackDesc = getOwnPropertyDescriptor(Error('er1'), 'stack'); +const er2StackDesc = getOwnPropertyDescriptor(TypeError('er2'), 'stack'); + +let feralStackGetter; +let feralStackSetter; +if (er1StackDesc && er2StackDesc && er1StackDesc.get) { + // We should only encounter this case on v8 because of its problematic + // error own stack accessor behavior. + // Note that FF/SpiderMonkey, Moddable/XS, and the error stack proposal + // all inherit a stack accessor property from Error.prototype, which is + // great. That case needs no heroics to secure. + if ( + // In the v8 case as we understand it, all errors have an own stack + // accessor property, but within the same realm, all these accessor + // properties have the same getter and have the same setter. + // This is therefore the case that we repair. + typeof er1StackDesc.get === 'function' && + er1StackDesc.get === er2StackDesc.get && + typeof er1StackDesc.set === 'function' && + er1StackDesc.set === er2StackDesc.set + ) { + // Otherwise, we have own stack accessor properties that are outside + // our expectations, that therefore need to be understood better + // before we know how to repair them. + feralStackGetter = freeze(typeErrorStackDesc.get); + feralStackSetter = freeze(typeErrorStackDesc.set); + } else { + // See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_UNEXPECTED_ERROR_OWN_STACK_ACCESSOR.md + throw TypeError( + 'Unexpected Error own stack accessor functions (SES_UNEXPECTED_ERROR_OWN_STACK_ACCESSOR)', + ); + } +} + +/** + * If on a v8 with the problematic error own stack accessor behavior, + * `FERAL_STACK_GETTER` will be the shared getter of all those accessors + * and `FERAL_STACK_SETTER` will be the shared setter. On any platform + * without this problem, `FERAL_STACK_GETTER` and `FERAL_STACK_SETTER` are + * both `undefined`. + * + * @type {(() => any) | undefined} + */ +export const FERAL_STACK_GETTER = feralStackGetter; + +/** + * If on a v8 with the problematic error own stack accessor behavior, + * `FERAL_STACK_GETTER` will be the shared getter of all those accessors + * and `FERAL_STACK_SETTER` will be the shared setter. On any platform + * without this problem, `FERAL_STACK_GETTER` and `FERAL_STACK_SETTER` are + * both `undefined`. + * + * @type {((newValue: any) => void) | undefined} + */ +export const FERAL_STACK_SETTER = feralStackSetter; + +const getAsyncGeneratorFunctionInstance = () => { + // Test for async generator function syntax support. + try { + // Wrapping one in an new Function lets the `hermesc` binary file + // parse the Metro js bundle without SyntaxError, to generate the + // optimised Hermes bytecode bundle, when `gradlew` is called to + // assemble the release build APK for React Native prod Android apps. + // Delaying the error until runtime lets us customise lockdown behaviour. + return new FERAL_FUNCTION( + 'return (async function* AsyncGeneratorFunctionInstance() {})', + )(); + } catch (error) { + // Note: `Error.prototype.jsEngine` is only set by React Native runtime, not Hermes: + // https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.cpp#L224-L230 + if (error.name === 'SyntaxError') { + // Swallows Hermes error `async generators are unsupported` at runtime. + // Note: `console` is not a JS built-in, so Hermes engine throws: + // Uncaught ReferenceError: Property 'console' doesn't exist + // See: https://github.com/facebook/hermes/issues/675 + // However React Native provides a `console` implementation when setting up error handling: + // https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/InitializeCore.js + return undefined; + } else if (error.name === 'EvalError') { + // eslint-disable-next-line no-empty-function + return async function* AsyncGeneratorFunctionInstance() {}; + } else { + throw error; + } + } +}; + +/** + * If the platform supports async generator functions, this will be an + * async generator function instance. Otherwise, it will be `undefined`. + * + * @type {AsyncGeneratorFunction | undefined} + */ +export const AsyncGeneratorFunctionInstance = + getAsyncGeneratorFunctionInstance(); /** @type {(condition: any) => asserts condition} */ const assert = condition => { From 9ae2f5341792f04d24720e6e4b1e5746dc6c53cb Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 9 Oct 2025 19:23:27 -0700 Subject: [PATCH 08/22] refactor(harden): Do not export inline commons of make-harden.js --- packages/harden/make-hardener.js | 210 ++++++++++++++++--------------- 1 file changed, 112 insertions(+), 98 deletions(-) diff --git a/packages/harden/make-hardener.js b/packages/harden/make-hardener.js index 5ce7e0b5b8..8ce20238a6 100644 --- a/packages/harden/make-hardener.js +++ b/packages/harden/make-hardener.js @@ -25,9 +25,8 @@ // We cannot use globalThis as the local name since it would capture the // lexical name. const universalThis = globalThis; -export { universalThis as globalThis }; -export const { +const { Array, ArrayBuffer, Date, @@ -50,7 +49,7 @@ export const { WeakSet, } = globalThis; -export const { +const { // The feral Error constructor is safe for internal use, but must not be // revealed to post-lockdown code in any compartment including the start // compartment since in V8 at least it bears stack inspection capabilities. @@ -62,7 +61,7 @@ export const { AggregateError, } = globalThis; -export const { +const { assign, create, defineProperties, @@ -85,7 +84,7 @@ export const { fromEntries, } = Object; -export const { +const { species: speciesSymbol, toStringTag: toStringTagSymbol, iterator: iteratorSymbol, @@ -95,14 +94,14 @@ export const { for: symbolFor, } = Symbol; -export const { isInteger } = Number; +const { isInteger } = Number; -export const { stringify: stringifyJson } = JSON; +const { stringify: stringifyJson } = JSON; // Needed only for the Safari bug workaround below const { defineProperty: originalDefineProperty } = Object; -export const defineProperty = (object, prop, descriptor) => { +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. @@ -123,7 +122,7 @@ export const defineProperty = (object, prop, descriptor) => { return result; }; -export const { +const { apply, construct, get: reflectGet, @@ -135,27 +134,27 @@ export const { set: reflectSet, } = Reflect; -export const { isArray, prototype: arrayPrototype } = Array; -export const { prototype: arrayBufferPrototype } = ArrayBuffer; -export const { prototype: mapPrototype } = Map; -export const { revocable: proxyRevocable } = Proxy; -export const { prototype: regexpPrototype } = RegExp; -export const { prototype: setPrototype } = Set; -export const { prototype: stringPrototype } = String; -export const { prototype: weakmapPrototype } = WeakMap; -export const { prototype: weaksetPrototype } = WeakSet; -export const { prototype: functionPrototype } = Function; -export const { prototype: promisePrototype } = Promise; -export const { prototype: generatorPrototype } = getPrototypeOf( +const { isArray, prototype: arrayPrototype } = Array; +const { prototype: arrayBufferPrototype } = ArrayBuffer; +const { prototype: mapPrototype } = Map; +const { revocable: proxyRevocable } = Proxy; +const { prototype: regexpPrototype } = RegExp; +const { prototype: setPrototype } = Set; +const { prototype: stringPrototype } = String; +const { prototype: weakmapPrototype } = WeakMap; +const { prototype: weaksetPrototype } = WeakSet; +const { prototype: functionPrototype } = Function; +const { prototype: promisePrototype } = Promise; +const { prototype: generatorPrototype } = getPrototypeOf( // eslint-disable-next-line no-empty-function, func-names function* () {}, ); -export const iteratorPrototype = getPrototypeOf( +const iteratorPrototype = getPrototypeOf( // eslint-disable-next-line @endo/no-polymorphic-call getPrototypeOf(arrayPrototype.values()), ); -export const typedArrayPrototype = getPrototypeOf(Uint8Array.prototype); +const typedArrayPrototype = getPrototypeOf(Uint8Array.prototype); const { bind } = functionPrototype; @@ -170,7 +169,7 @@ const { bind } = functionPrototype; * * @type { any>(fn: F) => ((thisArg: ThisParameterType, ...args: Parameters) => ReturnType)} */ -export const uncurryThis = bind.bind(bind.call); // eslint-disable-line @endo/no-polymorphic-call +const uncurryThis = bind.bind(bind.call); // eslint-disable-line @endo/no-polymorphic-call // See https://github.com/endojs/endo/issues/2930 if (!('hasOwn' in Object)) { @@ -192,62 +191,62 @@ if (!('hasOwn' in Object)) { }); } -export const { hasOwn } = Object; +const { hasOwn } = Object; /** * @deprecated Use `hasOwn` instead */ -export const objectHasOwnProperty = hasOwn; +const objectHasOwnProperty = hasOwn; // -export const arrayFilter = uncurryThis(arrayPrototype.filter); -export const arrayForEach = uncurryThis(arrayPrototype.forEach); -export const arrayIncludes = uncurryThis(arrayPrototype.includes); -export const arrayJoin = uncurryThis(arrayPrototype.join); +const arrayFilter = uncurryThis(arrayPrototype.filter); +const arrayForEach = uncurryThis(arrayPrototype.forEach); +const arrayIncludes = uncurryThis(arrayPrototype.includes); +const arrayJoin = uncurryThis(arrayPrototype.join); /** @type {(thisArg: 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} */ ( +const arrayMap = /** @type {any} */ (uncurryThis(arrayPrototype.map)); +const arrayFlatMap = /** @type {any} */ ( uncurryThis(arrayPrototype.flatMap) ); -export const arrayPop = uncurryThis(arrayPrototype.pop); +const arrayPop = uncurryThis(arrayPrototype.pop); /** @type {(thisArg: T[], ...items: T[]) => number} */ -export const arrayPush = uncurryThis(arrayPrototype.push); -export const arraySlice = uncurryThis(arrayPrototype.slice); -export const arraySome = uncurryThis(arrayPrototype.some); -export const arraySort = uncurryThis(arrayPrototype.sort); -export const iterateArray = uncurryThis(arrayPrototype[iteratorSymbol]); +const arrayPush = uncurryThis(arrayPrototype.push); +const arraySlice = uncurryThis(arrayPrototype.slice); +const arraySome = uncurryThis(arrayPrototype.some); +const arraySort = uncurryThis(arrayPrototype.sort); +const iterateArray = uncurryThis(arrayPrototype[iteratorSymbol]); // -export const arrayBufferSlice = uncurryThis(arrayBufferPrototype.slice); +const arrayBufferSlice = uncurryThis(arrayBufferPrototype.slice); /** @type {(b: ArrayBuffer) => number} */ -export const arrayBufferGetByteLength = uncurryThis( +const arrayBufferGetByteLength = uncurryThis( // @ts-expect-error we know it is there on all conforming platforms getOwnPropertyDescriptor(arrayBufferPrototype, 'byteLength').get, ); // -export const typedArraySet = uncurryThis(typedArrayPrototype.set); +const typedArraySet = uncurryThis(typedArrayPrototype.set); // -export const mapSet = uncurryThis(mapPrototype.set); -export const mapGet = uncurryThis(mapPrototype.get); -export const mapHas = uncurryThis(mapPrototype.has); -export const mapDelete = uncurryThis(mapPrototype.delete); -export const mapEntries = uncurryThis(mapPrototype.entries); -export const iterateMap = uncurryThis(mapPrototype[iteratorSymbol]); +const mapSet = uncurryThis(mapPrototype.set); +const mapGet = uncurryThis(mapPrototype.get); +const mapHas = uncurryThis(mapPrototype.has); +const mapDelete = uncurryThis(mapPrototype.delete); +const mapEntries = uncurryThis(mapPrototype.entries); +const iterateMap = uncurryThis(mapPrototype[iteratorSymbol]); // -export const setAdd = uncurryThis(setPrototype.add); -export const setDelete = uncurryThis(setPrototype.delete); -export const setForEach = uncurryThis(setPrototype.forEach); -export const setHas = uncurryThis(setPrototype.has); -export const iterateSet = uncurryThis(setPrototype[iteratorSymbol]); +const setAdd = uncurryThis(setPrototype.add); +const setDelete = uncurryThis(setPrototype.delete); +const setForEach = uncurryThis(setPrototype.forEach); +const setHas = uncurryThis(setPrototype.has); +const iterateSet = uncurryThis(setPrototype[iteratorSymbol]); // -export const regexpTest = uncurryThis(regexpPrototype.test); -export const regexpExec = uncurryThis(regexpPrototype.exec); -export const matchAllRegExp = uncurryThis(regexpPrototype[matchAllSymbol]); +const regexpTest = uncurryThis(regexpPrototype.test); +const regexpExec = uncurryThis(regexpPrototype.exec); +const matchAllRegExp = uncurryThis(regexpPrototype[matchAllSymbol]); // -export const stringEndsWith = uncurryThis(stringPrototype.endsWith); -export const stringIncludes = uncurryThis(stringPrototype.includes); -export const stringIndexOf = uncurryThis(stringPrototype.indexOf); -export const stringMatch = uncurryThis(stringPrototype.match); -export const generatorNext = uncurryThis(generatorPrototype.next); -export const generatorThrow = uncurryThis(generatorPrototype.throw); +const stringEndsWith = uncurryThis(stringPrototype.endsWith); +const stringIncludes = uncurryThis(stringPrototype.includes); +const stringIndexOf = uncurryThis(stringPrototype.indexOf); +const stringMatch = uncurryThis(stringPrototype.match); +const generatorNext = uncurryThis(generatorPrototype.next); +const generatorThrow = uncurryThis(generatorPrototype.throw); /** * @type { & @@ -255,41 +254,41 @@ export const generatorThrow = uncurryThis(generatorPrototype.throw); * ((thisArg: 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} */ ( +const stringReplace = /** @type {any} */ ( uncurryThis(stringPrototype.replace) ); -export const stringSearch = uncurryThis(stringPrototype.search); -export const stringSlice = uncurryThis(stringPrototype.slice); -export const stringSplit = +const stringSearch = uncurryThis(stringPrototype.search); +const stringSlice = uncurryThis(stringPrototype.slice); +const stringSplit = /** @type {(thisArg: string, splitter: string | RegExp | { [Symbol.split](string: string, limit?: number): string[]; }, limit?: number) => string[]} */ ( uncurryThis(stringPrototype.split) ); -export const stringStartsWith = uncurryThis(stringPrototype.startsWith); -export const iterateString = uncurryThis(stringPrototype[iteratorSymbol]); +const stringStartsWith = uncurryThis(stringPrototype.startsWith); +const iterateString = uncurryThis(stringPrototype[iteratorSymbol]); // -export const weakmapDelete = uncurryThis(weakmapPrototype.delete); +const weakmapDelete = uncurryThis(weakmapPrototype.delete); /** @type {(thisArg: WeakMap, ...args: Parameters['get']>) => ReturnType['get']>} */ -export const weakmapGet = uncurryThis(weakmapPrototype.get); -export const weakmapHas = uncurryThis(weakmapPrototype.has); -export const weakmapSet = uncurryThis(weakmapPrototype.set); +const weakmapGet = uncurryThis(weakmapPrototype.get); +const weakmapHas = uncurryThis(weakmapPrototype.has); +const weakmapSet = uncurryThis(weakmapPrototype.set); // -export const weaksetAdd = uncurryThis(weaksetPrototype.add); -export const weaksetHas = uncurryThis(weaksetPrototype.has); +const weaksetAdd = uncurryThis(weaksetPrototype.add); +const weaksetHas = uncurryThis(weaksetPrototype.has); // -export const functionToString = uncurryThis(functionPrototype.toString); -export const functionBind = uncurryThis(bind); +const functionToString = uncurryThis(functionPrototype.toString); +const functionBind = uncurryThis(bind); // const { all } = Promise; -export const promiseAll = promises => apply(all, Promise, [promises]); -export const promiseCatch = uncurryThis(promisePrototype.catch); +const promiseAll = promises => apply(all, Promise, [promises]); +const promiseCatch = uncurryThis(promisePrototype.catch); /** @type {(thisArg: T, onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null) => Promise} */ -export const promiseThen = /** @type {any} */ ( +const promiseThen = /** @type {any} */ ( uncurryThis(promisePrototype.then) ); // -export const finalizationRegistryRegister = +const finalizationRegistryRegister = FinalizationRegistry && uncurryThis(FinalizationRegistry.prototype.register); -export const finalizationRegistryUnregister = +const finalizationRegistryUnregister = FinalizationRegistry && uncurryThis(FinalizationRegistry.prototype.unregister); @@ -299,7 +298,7 @@ export const finalizationRegistryUnregister = * * @param {Function} fn */ -export const getConstructorOf = fn => +const getConstructorOf = fn => reflectGet(getPrototypeOf(fn), 'constructor'); /** @@ -315,7 +314,7 @@ export const getConstructorOf = fn => * | string * | symbol)} */ -export const isPrimitive = val => +const isPrimitive = val => !val || (typeof val !== 'object' && typeof val !== 'function'); /** @@ -329,38 +328,52 @@ export const isPrimitive = val => * * @param {any} value */ -export const isError = value => value instanceof FERAL_ERROR; +const isError = value => value instanceof FERAL_ERROR; /** * @template T * @param {T} x */ -export const identity = x => x; +const identity = x => x; // The original unsafe untamed eval function, which must not escape. // Sample at module initialization time, which is before lockdown can // repair it. Use it only to build powerless abstractions. // eslint-disable-next-line no-eval -export const FERAL_EVAL = eval; +const FERAL_EVAL = eval; // The original unsafe untamed Function constructor, which must not escape. // Sample at module initialization time, which is before lockdown can // repair it. Use it only to build powerless abstractions. -export const FERAL_FUNCTION = Function; +const FERAL_FUNCTION = Function; -export const noEvalEvaluate = () => { +const noEvalEvaluate = () => { // See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_NO_EVAL.md throw TypeError('Cannot eval with evalTaming set to "no-eval" (SES_NO_EVAL)'); }; // ////////////////// FERAL_STACK_GETTER FERAL_STACK_SETTER //////////////////// -const er1StackDesc = getOwnPropertyDescriptor(Error('er1'), 'stack'); -const er2StackDesc = getOwnPropertyDescriptor(TypeError('er2'), 'stack'); +// The error repair mechanism is very similar to code in ses/src/commons.js +// and these implementations should be kept in sync. + +const makeTypeError = () => { + try { + // @ts-expect-error deliberate TypeError + null.null; + throw TypeError('obligatory'); // To convince the type flow inferrence. + } catch (error) { + return error; + } +}; + +const typeErrorStackDesc = getOwnPropertyDescriptor(makeTypeError(), 'stack'); +const errorStackDesc = getOwnPropertyDescriptor(Error('obligatory'), 'stack'); let feralStackGetter; let feralStackSetter; -if (er1StackDesc && er2StackDesc && er1StackDesc.get) { + +if (typeErrorStackDesc !== undefined && typeErrorStackDesc.get !== undefined) { // We should only encounter this case on v8 because of its problematic // error own stack accessor behavior. // Note that FF/SpiderMonkey, Moddable/XS, and the error stack proposal @@ -371,10 +384,11 @@ if (er1StackDesc && er2StackDesc && er1StackDesc.get) { // accessor property, but within the same realm, all these accessor // properties have the same getter and have the same setter. // This is therefore the case that we repair. - typeof er1StackDesc.get === 'function' && - er1StackDesc.get === er2StackDesc.get && - typeof er1StackDesc.set === 'function' && - er1StackDesc.set === er2StackDesc.set + errorStackDesc !== undefined && + typeof typeErrorStackDesc.get === 'function' && + typeErrorStackDesc.get === errorStackDesc.get && + typeof typeErrorStackDesc.set === 'function' && + typeErrorStackDesc.set === errorStackDesc.set ) { // Otherwise, we have own stack accessor properties that are outside // our expectations, that therefore need to be understood better @@ -398,7 +412,7 @@ if (er1StackDesc && er2StackDesc && er1StackDesc.get) { * * @type {(() => any) | undefined} */ -export const FERAL_STACK_GETTER = feralStackGetter; +const FERAL_STACK_GETTER = feralStackGetter; /** * If on a v8 with the problematic error own stack accessor behavior, @@ -409,7 +423,7 @@ export const FERAL_STACK_GETTER = feralStackGetter; * * @type {((newValue: any) => void) | undefined} */ -export const FERAL_STACK_SETTER = feralStackSetter; +const FERAL_STACK_SETTER = feralStackSetter; const getAsyncGeneratorFunctionInstance = () => { // Test for async generator function syntax support. @@ -448,7 +462,7 @@ const getAsyncGeneratorFunctionInstance = () => { * * @type {AsyncGeneratorFunction | undefined} */ -export const AsyncGeneratorFunctionInstance = +const AsyncGeneratorFunctionInstance = getAsyncGeneratorFunctionInstance(); /** @type {(condition: any) => asserts condition} */ @@ -478,7 +492,7 @@ assert(getTypedArrayToStringTag); * * @param {unknown} object */ -export const isTypedArray = object => { +const isTypedArray = object => { // The object must pass a brand check or toStringTag will return undefined. const tag = apply(getTypedArrayToStringTag, object, []); return tag !== undefined; From ac43c782a987052e97d1635d3997cafcf6b45dc2 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 07:58:01 -0700 Subject: [PATCH 09/22] refactor(harden): Inline Harden type in make-hardener.js --- packages/harden/make-hardener.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/harden/make-hardener.js b/packages/harden/make-hardener.js index 8ce20238a6..e2e0e94561 100644 --- a/packages/harden/make-hardener.js +++ b/packages/harden/make-hardener.js @@ -473,7 +473,8 @@ const assert = condition => { }; /** - * @import {Harden} from '../types.js' + * @template T + * @typedef {(value: T) => T} Harden */ // Obtain the string tag accessor of of TypedArray so we can indirectly use the @@ -545,9 +546,10 @@ const freezeTypedArray = array => { /** * Create a `harden` function. * + * @template T * @param {object} [args] * @param {boolean} [args.traversePrototypes] - * @returns {Harden} + * @returns {Harden} */ export const makeHardener = ({ traversePrototypes = false } = {}) => { const hardened = new WeakSet(); From 535c437bac21f922f1218bb2543d8fae9539ea7b Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 07:05:47 -0700 Subject: [PATCH 10/22] refactor(harden): Remove unused commons in make-hardener --- packages/harden/make-hardener.js | 238 +------------------------------ 1 file changed, 7 insertions(+), 231 deletions(-) diff --git a/packages/harden/make-hardener.js b/packages/harden/make-hardener.js index e2e0e94561..5306f1cb3a 100644 --- a/packages/harden/make-hardener.js +++ b/packages/harden/make-hardener.js @@ -1,4 +1,3 @@ - // Adapted from SES/Caja - Copyright (C) 2011 Google Inc. // Copyright (C) 2018 Agoric @@ -22,30 +21,18 @@ // @ts-check -// We cannot use globalThis as the local name since it would capture the -// lexical name. -const universalThis = globalThis; +/* global globalThis */ const { Array, - ArrayBuffer, - Date, - FinalizationRegistry, - Float32Array, JSON, - Map, - Math, Number, Object, - Promise, - Proxy, Reflect, - RegExp: FERAL_REG_EXP, Set, String, Symbol, Uint8Array, - WeakMap, WeakSet, } = globalThis; @@ -54,45 +41,19 @@ const { // revealed to post-lockdown code in any compartment including the start // compartment since in V8 at least it bears stack inspection capabilities. Error: FERAL_ERROR, - RangeError, - ReferenceError, - SyntaxError, TypeError, - AggregateError, } = globalThis; const { - assign, - create, - defineProperties, - entries, freeze, getOwnPropertyDescriptor, getOwnPropertyDescriptors, - getOwnPropertyNames, getPrototypeOf, - is, - isFrozen, - isSealed, - isExtensible, - keys, prototype: objectPrototype, - seal, preventExtensions, - setPrototypeOf, - values, - fromEntries, } = Object; -const { - species: speciesSymbol, - toStringTag: toStringTagSymbol, - iterator: iteratorSymbol, - matchAll: matchAllSymbol, - unscopables: unscopablesSymbol, - keyFor: symbolKeyFor, - for: symbolFor, -} = Symbol; +const { toStringTag: toStringTagSymbol } = Symbol; const { isInteger } = Number; @@ -122,37 +83,12 @@ const defineProperty = (object, prop, descriptor) => { return result; }; -const { - apply, - construct, - get: reflectGet, - getOwnPropertyDescriptor: reflectGetOwnPropertyDescriptor, - has: reflectHas, - isExtensible: reflectIsExtensible, - ownKeys, - preventExtensions: reflectPreventExtensions, - set: reflectSet, -} = Reflect; - -const { isArray, prototype: arrayPrototype } = Array; -const { prototype: arrayBufferPrototype } = ArrayBuffer; -const { prototype: mapPrototype } = Map; -const { revocable: proxyRevocable } = Proxy; -const { prototype: regexpPrototype } = RegExp; +const { apply, ownKeys } = Reflect; + +const { prototype: arrayPrototype } = Array; const { prototype: setPrototype } = Set; -const { prototype: stringPrototype } = String; -const { prototype: weakmapPrototype } = WeakMap; const { prototype: weaksetPrototype } = WeakSet; const { prototype: functionPrototype } = Function; -const { prototype: promisePrototype } = Promise; -const { prototype: generatorPrototype } = getPrototypeOf( - // eslint-disable-next-line no-empty-function, func-names - function* () {}, -); -const iteratorPrototype = getPrototypeOf( - // eslint-disable-next-line @endo/no-polymorphic-call - getPrototypeOf(arrayPrototype.values()), -); const typedArrayPrototype = getPrototypeOf(Uint8Array.prototype); @@ -193,116 +129,18 @@ if (!('hasOwn' in Object)) { const { hasOwn } = Object; -/** - * @deprecated Use `hasOwn` instead - */ -const objectHasOwnProperty = hasOwn; -// -const arrayFilter = uncurryThis(arrayPrototype.filter); const arrayForEach = uncurryThis(arrayPrototype.forEach); -const arrayIncludes = uncurryThis(arrayPrototype.includes); -const arrayJoin = uncurryThis(arrayPrototype.join); -/** @type {(thisArg: readonly T[], callbackfn: (value: T, index: number, array: T[]) => U, cbThisArg?: any) => U[]} */ -const arrayMap = /** @type {any} */ (uncurryThis(arrayPrototype.map)); -const arrayFlatMap = /** @type {any} */ ( - uncurryThis(arrayPrototype.flatMap) -); -const arrayPop = uncurryThis(arrayPrototype.pop); -/** @type {(thisArg: T[], ...items: T[]) => number} */ -const arrayPush = uncurryThis(arrayPrototype.push); -const arraySlice = uncurryThis(arrayPrototype.slice); -const arraySome = uncurryThis(arrayPrototype.some); -const arraySort = uncurryThis(arrayPrototype.sort); -const iterateArray = uncurryThis(arrayPrototype[iteratorSymbol]); -// -const arrayBufferSlice = uncurryThis(arrayBufferPrototype.slice); -/** @type {(b: ArrayBuffer) => number} */ -const arrayBufferGetByteLength = uncurryThis( - // @ts-expect-error we know it is there on all conforming platforms - getOwnPropertyDescriptor(arrayBufferPrototype, 'byteLength').get, -); -// -const typedArraySet = uncurryThis(typedArrayPrototype.set); -// -const mapSet = uncurryThis(mapPrototype.set); -const mapGet = uncurryThis(mapPrototype.get); -const mapHas = uncurryThis(mapPrototype.has); -const mapDelete = uncurryThis(mapPrototype.delete); -const mapEntries = uncurryThis(mapPrototype.entries); -const iterateMap = uncurryThis(mapPrototype[iteratorSymbol]); // const setAdd = uncurryThis(setPrototype.add); -const setDelete = uncurryThis(setPrototype.delete); const setForEach = uncurryThis(setPrototype.forEach); const setHas = uncurryThis(setPrototype.has); -const iterateSet = uncurryThis(setPrototype[iteratorSymbol]); -// -const regexpTest = uncurryThis(regexpPrototype.test); -const regexpExec = uncurryThis(regexpPrototype.exec); -const matchAllRegExp = uncurryThis(regexpPrototype[matchAllSymbol]); -// -const stringEndsWith = uncurryThis(stringPrototype.endsWith); -const stringIncludes = uncurryThis(stringPrototype.includes); -const stringIndexOf = uncurryThis(stringPrototype.indexOf); -const stringMatch = uncurryThis(stringPrototype.match); -const generatorNext = uncurryThis(generatorPrototype.next); -const generatorThrow = uncurryThis(generatorPrototype.throw); -/** - * @type { & - * ((thisArg: string, searchValue: { [Symbol.replace](string: string, replaceValue: string): string; }, replaceValue: string) => string) & - * ((thisArg: string, searchValue: { [Symbol.replace](string: string, replacer: (substring: string, ...args: any[]) => string): string; }, replacer: (substring: string, ...args: any[]) => string) => string) - * } - */ -const stringReplace = /** @type {any} */ ( - uncurryThis(stringPrototype.replace) -); -const stringSearch = uncurryThis(stringPrototype.search); -const stringSlice = uncurryThis(stringPrototype.slice); -const stringSplit = - /** @type {(thisArg: string, splitter: string | RegExp | { [Symbol.split](string: string, limit?: number): string[]; }, limit?: number) => string[]} */ ( - uncurryThis(stringPrototype.split) - ); -const stringStartsWith = uncurryThis(stringPrototype.startsWith); -const iterateString = uncurryThis(stringPrototype[iteratorSymbol]); -// -const weakmapDelete = uncurryThis(weakmapPrototype.delete); -/** @type {(thisArg: WeakMap, ...args: Parameters['get']>) => ReturnType['get']>} */ -const weakmapGet = uncurryThis(weakmapPrototype.get); -const weakmapHas = uncurryThis(weakmapPrototype.has); -const weakmapSet = uncurryThis(weakmapPrototype.set); -// const weaksetAdd = uncurryThis(weaksetPrototype.add); const weaksetHas = uncurryThis(weaksetPrototype.has); -// -const functionToString = uncurryThis(functionPrototype.toString); -const functionBind = uncurryThis(bind); -// -const { all } = Promise; -const promiseAll = promises => apply(all, Promise, [promises]); -const promiseCatch = uncurryThis(promisePrototype.catch); -/** @type {(thisArg: T, onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null) => Promise} */ -const promiseThen = /** @type {any} */ ( - uncurryThis(promisePrototype.then) -); -// -const finalizationRegistryRegister = - FinalizationRegistry && uncurryThis(FinalizationRegistry.prototype.register); -const finalizationRegistryUnregister = - FinalizationRegistry && - uncurryThis(FinalizationRegistry.prototype.unregister); /** - * getConstructorOf() - * Return the constructor from an instance. - * - * @param {Function} fn - */ -const getConstructorOf = fn => - reflectGet(getPrototypeOf(fn), 'constructor'); - -/** - * TODO Consolidate with `isPrimitive` that's currently in `@endo/pass-style`. + * TODO Consolidate with `isPrimitive` that's currently in `@endo/pass-style` + * and also `ses`. * Layering constraints make this tricky, which is why we haven't yet figured * out how to do this. * @@ -330,28 +168,6 @@ const isPrimitive = val => */ const isError = value => value instanceof FERAL_ERROR; -/** - * @template T - * @param {T} x - */ -const identity = x => x; - -// The original unsafe untamed eval function, which must not escape. -// Sample at module initialization time, which is before lockdown can -// repair it. Use it only to build powerless abstractions. -// eslint-disable-next-line no-eval -const FERAL_EVAL = eval; - -// The original unsafe untamed Function constructor, which must not escape. -// Sample at module initialization time, which is before lockdown can -// repair it. Use it only to build powerless abstractions. -const FERAL_FUNCTION = Function; - -const noEvalEvaluate = () => { - // See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_NO_EVAL.md - throw TypeError('Cannot eval with evalTaming set to "no-eval" (SES_NO_EVAL)'); -}; - // ////////////////// FERAL_STACK_GETTER FERAL_STACK_SETTER //////////////////// // The error repair mechanism is very similar to code in ses/src/commons.js @@ -425,46 +241,6 @@ const FERAL_STACK_GETTER = feralStackGetter; */ const FERAL_STACK_SETTER = feralStackSetter; -const getAsyncGeneratorFunctionInstance = () => { - // Test for async generator function syntax support. - try { - // Wrapping one in an new Function lets the `hermesc` binary file - // parse the Metro js bundle without SyntaxError, to generate the - // optimised Hermes bytecode bundle, when `gradlew` is called to - // assemble the release build APK for React Native prod Android apps. - // Delaying the error until runtime lets us customise lockdown behaviour. - return new FERAL_FUNCTION( - 'return (async function* AsyncGeneratorFunctionInstance() {})', - )(); - } catch (error) { - // Note: `Error.prototype.jsEngine` is only set by React Native runtime, not Hermes: - // https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.cpp#L224-L230 - if (error.name === 'SyntaxError') { - // Swallows Hermes error `async generators are unsupported` at runtime. - // Note: `console` is not a JS built-in, so Hermes engine throws: - // Uncaught ReferenceError: Property 'console' doesn't exist - // See: https://github.com/facebook/hermes/issues/675 - // However React Native provides a `console` implementation when setting up error handling: - // https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/InitializeCore.js - return undefined; - } else if (error.name === 'EvalError') { - // eslint-disable-next-line no-empty-function - return async function* AsyncGeneratorFunctionInstance() {}; - } else { - throw error; - } - } -}; - -/** - * If the platform supports async generator functions, this will be an - * async generator function instance. Otherwise, it will be `undefined`. - * - * @type {AsyncGeneratorFunction | undefined} - */ -const AsyncGeneratorFunctionInstance = - getAsyncGeneratorFunctionInstance(); - /** @type {(condition: any) => asserts condition} */ const assert = condition => { if (!condition) { From 23a4109216cf1f6857dd4395b76a607e4049818a Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 9 Oct 2025 19:35:32 -0700 Subject: [PATCH 11/22] feat(harden): Add mechanism for detecting, providing, installing global harden --- packages/harden/make-selector.js | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/harden/make-selector.js diff --git a/packages/harden/make-selector.js b/packages/harden/make-selector.js new file mode 100644 index 0000000000..c18191a51d --- /dev/null +++ b/packages/harden/make-selector.js @@ -0,0 +1,68 @@ +/* This module provides the mechanism used by both the "unsafe" and "shallow" + * (default) implementations of "@endo/harden" for racing to install an + * implementation of harden at globalThis.harden and + * Object[Symbol.for('harden')]. + */ + +/* global globalThis */ + +/** @import { Harden } from './make-hardener.js' */ + +const symbolForHarden = Symbol.for('harden'); + +/** + * @template T + * @param {() => Harden} makeHardener + */ +export const makeHardenerSelector = makeHardener => { + const selectHarden = () => { + // @ts-expect-error Type 'unique symbol' cannot be used as an index type. + const { [symbolForHarden]: objectHarden } = Object; + if (objectHarden) { + if (typeof objectHarden !== 'function') { + throw new Error('@endo/harden expected callable Object[@harden]'); + } + return objectHarden; + } + + const { harden: globalHarden } = globalThis; + if (globalHarden) { + if (typeof globalHarden !== 'function') { + throw new Error('@endo/harden expected callable globalThis.harden'); + } + return globalHarden; + } + + const harden = makeHardener(); + // We should not reach this point if a harden implementation already exists here. + // The non-configurability of this property will prevent any HardenedJS's + // lockdown from succeeding. + // Versions that predate the introduction of Object[@harden] will be unable + // to remove the unknown intrinsic. + // Versions that permit Object[@harden] fail explicitly. + Object.defineProperty(Object, symbolForHarden, { + value: harden, + configurable: false, + writable: false, + }); + + return harden; + }; + + let selectedHarden; + + /** + * @template T + * @param {T} object + * @returns {T} + */ + const harden = object => { + if (!selectedHarden) { + selectedHarden = selectHarden(); + } + return selectedHarden(object); + }; + Object.freeze(harden); + + return harden; +}; From 741c9a2e8a3c32c55129cbdb2944e96982f184af Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 07:10:30 -0700 Subject: [PATCH 12/22] feat(harden): Presumed hardened mode --- packages/harden/hardened.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/harden/hardened.js diff --git a/packages/harden/hardened.js b/packages/harden/hardened.js new file mode 100644 index 0000000000..fc1ff9e5c9 --- /dev/null +++ b/packages/harden/hardened.js @@ -0,0 +1,18 @@ +/* This implementation of harden asserts that there is a harden implementation + * on the global or shared intrinsics as provided by a valid HardenedJS + * environment. + * Select this implementation of @endo/harden with -C hardened + * with tools like Node.js or Endo's bundle-source bundler. + */ + +/* global globalThis */ + +const harden = Object[Symbol.for('harden')] ?? globalThis.harden; + +if (harden === undefined) { + throw new Error( + 'Cannot initialize @endo/harden. This program was initialized with the "hardened" condition (-C hardened) but not executed in a hardened JavaScript environment', + ); +} + +export default harden; From d047b729da54765e671fd7e1ae256b0720b5e6ba Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 9 Oct 2025 19:37:26 -0700 Subject: [PATCH 13/22] feat(harden): Unsafe harden mode --- packages/harden/unsafe.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/harden/unsafe.js diff --git a/packages/harden/unsafe.js b/packages/harden/unsafe.js new file mode 100644 index 0000000000..8389df1adb --- /dev/null +++ b/packages/harden/unsafe.js @@ -0,0 +1,25 @@ +/* This implementation of harden first senses and provides the implementation + * of harden present in the global object tor shared intrinsics. + * Failing to find an existing implementation, this provides and installs a + * fake version that does nothing. + * This version can be selected with the package.json condition + * "harden:unsafe", as with node -C harden:unsafe or Endo's bundle-source -C + * harden:unsafe. + */ + +import { makeHardenerSelector } from './make-selector.js'; + +const makeFakeHarden = () => { + const harden = o => o; + harden.isFake = true; + harden.lockdownError = new Error( + 'Cannot lockdown (repairIntrinsics) because @endo/harden used before lockdown on this stack', + ); + return harden; +}; + +const { harden, isFrozenIfLockdown } = makeHardenerSelector(makeFakeHarden); + +export default harden; + +export { isFrozenIfLockdown }; From 6ef5c11e08ec58ebc6e84dca53a00bef2a39ca57 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 07:06:54 -0700 Subject: [PATCH 14/22] feat(harden): Shallow harden mode --- packages/harden/shallow.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/harden/shallow.js diff --git a/packages/harden/shallow.js b/packages/harden/shallow.js new file mode 100644 index 0000000000..8df9906d43 --- /dev/null +++ b/packages/harden/shallow.js @@ -0,0 +1,17 @@ +/* This implementation of harden first senses and provides the implementation + * of harden present in the global object or shared intrinsics. + * Failing to find an existing implementation, this provides and installs + * a version of harden that freezes the transitive own properties, not + * traversing prototypes. + * This preserves the mutability of the realm if used outside HardenedJS. + * + * This is the default implementation. + */ + +import { makeHardener } from './make-hardener.js'; +import { makeHardenerSelector } from './make-selector.js'; + +const harden = makeHardenerSelector(() => + makeHardener({ traversePrototypes: false }), +); +export default harden; From 3c4cba4dce6975d9a8259ada23efe9d6264a5ea6 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 07:11:51 -0700 Subject: [PATCH 15/22] feat(harden): Export three modes --- packages/harden/package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/harden/package.json b/packages/harden/package.json index 0028ceeb89..aad7469c0f 100644 --- a/packages/harden/package.json +++ b/packages/harden/package.json @@ -18,7 +18,12 @@ "main": "./index.js", "module": "./index.js", "exports": { - ".": "./index.js", + ".": { + "hardened": "./hardened.js", + "noop-harden": "./noop.js", + "default": "./shallow.js" + }, + "./is-noop.js": "./is-noop.js", "./package.json": "./package.json" }, "scripts": { From 9ece126c9150245104c0bce60e6f15cc5367101c Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 07:12:03 -0700 Subject: [PATCH 16/22] test(harden): Test all modes --- packages/harden/is-noop.js | 22 +++++++++++++++++++ packages/harden/{unsafe.js => noop.js} | 9 ++------ packages/harden/test/_lockdown.js | 1 + ...ssume-hardened-hardens-in-hardened.test.js | 16 ++++++++++++++ ...me-hardened-throws-in-non-hardened.test.js | 8 +++++++ .../noop-causes-lockdown-to-throw.test.js | 17 ++++++++++++++ ...op-does-not-freeze-before-lockdown.test.js | 8 +++++++ .../test/noop-freezes-after-lockdown.test.js | 18 +++++++++++++++ packages/harden/test/shallow-harden.test.js | 14 ++++++++++++ 9 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 packages/harden/is-noop.js rename packages/harden/{unsafe.js => noop.js} (77%) create mode 100644 packages/harden/test/_lockdown.js create mode 100644 packages/harden/test/assume-hardened-hardens-in-hardened.test.js create mode 100644 packages/harden/test/assume-hardened-throws-in-non-hardened.test.js create mode 100644 packages/harden/test/noop-causes-lockdown-to-throw.test.js create mode 100644 packages/harden/test/noop-does-not-freeze-before-lockdown.test.js create mode 100644 packages/harden/test/noop-freezes-after-lockdown.test.js create mode 100644 packages/harden/test/shallow-harden.test.js diff --git a/packages/harden/is-noop.js b/packages/harden/is-noop.js new file mode 100644 index 0000000000..a8e1a3fab5 --- /dev/null +++ b/packages/harden/is-noop.js @@ -0,0 +1,22 @@ +const { getOwnPropertyDescriptor } = Object; + +const memo = new WeakMap(); + +/** + * Empirically determines whether the `harden` exported by `@endo/harden` + * is a noop harden. + * @param {(object: object) => boolean} harden + */ +const hardenIsNoop = harden => { + let isNoop = memo.get(harden); + if (isNoop !== undefined) return isNoop; + // We do not trust isFrozen because lockdown with unsafe hardenTaming replaces + // isFrozen with a version that is in cahoots with fake harden. + const subject = harden({ __proto__: null, x: 0 }); + const desc = getOwnPropertyDescriptor(subject, 'x'); + isNoop = desc?.writable === true; + memo.set(harden, isNoop); + return isNoop; +}; + +export default hardenIsNoop; diff --git a/packages/harden/unsafe.js b/packages/harden/noop.js similarity index 77% rename from packages/harden/unsafe.js rename to packages/harden/noop.js index 8389df1adb..a81a8ec6a8 100644 --- a/packages/harden/unsafe.js +++ b/packages/harden/noop.js @@ -9,17 +9,12 @@ import { makeHardenerSelector } from './make-selector.js'; -const makeFakeHarden = () => { +const makeNoopHarden = () => { const harden = o => o; - harden.isFake = true; harden.lockdownError = new Error( 'Cannot lockdown (repairIntrinsics) because @endo/harden used before lockdown on this stack', ); return harden; }; -const { harden, isFrozenIfLockdown } = makeHardenerSelector(makeFakeHarden); - -export default harden; - -export { isFrozenIfLockdown }; +export default makeHardenerSelector(makeNoopHarden); diff --git a/packages/harden/test/_lockdown.js b/packages/harden/test/_lockdown.js new file mode 100644 index 0000000000..c9ba112b12 --- /dev/null +++ b/packages/harden/test/_lockdown.js @@ -0,0 +1 @@ +lockdown(); diff --git a/packages/harden/test/assume-hardened-hardens-in-hardened.test.js b/packages/harden/test/assume-hardened-hardens-in-hardened.test.js new file mode 100644 index 0000000000..1f87c2137e --- /dev/null +++ b/packages/harden/test/assume-hardened-hardens-in-hardened.test.js @@ -0,0 +1,16 @@ +import 'ses'; +import test from 'ava'; +import './_lockdown.js'; +import harden from '../hardened.js'; + +test('presume-hardened harden hardens after lockdown', t => { + t.true(Object.isFrozen(Object.prototype)); + const parent = { __proto__: {}, child: {} }; + t.false(Object.isFrozen(parent)); + t.false(Object.isFrozen(parent.child)); + t.false(Object.isFrozen(Object.getPrototypeOf(parent))); + harden(parent); + t.true(Object.isFrozen(parent)); + t.true(Object.isFrozen(parent.child)); + t.true(Object.isFrozen(Object.getPrototypeOf(parent))); +}); diff --git a/packages/harden/test/assume-hardened-throws-in-non-hardened.test.js b/packages/harden/test/assume-hardened-throws-in-non-hardened.test.js new file mode 100644 index 0000000000..0ad213c005 --- /dev/null +++ b/packages/harden/test/assume-hardened-throws-in-non-hardened.test.js @@ -0,0 +1,8 @@ +import test from 'ava'; + +test('presumed-hardened harden throws in non-hardened environment', async t => { + await t.throwsAsync(() => import('../hardened.js'), { + message: + 'Cannot initialize @endo/harden. This program was initialized with the "hardened" condition (-C hardened) but not executed in a hardened JavaScript environment', + }); +}); diff --git a/packages/harden/test/noop-causes-lockdown-to-throw.test.js b/packages/harden/test/noop-causes-lockdown-to-throw.test.js new file mode 100644 index 0000000000..f9957301f8 --- /dev/null +++ b/packages/harden/test/noop-causes-lockdown-to-throw.test.js @@ -0,0 +1,17 @@ +import 'ses'; +import test from 'ava'; +import harden from '../noop.js'; + +// This test is framed as a stand-alone module because calling `lockdown` has +// side-effects on the realm. + +test('lockdown throws if harden is used before', t => { + const object = {}; + harden(object); + t.false(Object.isFrozen(object)); + + t.throws(() => lockdown(), { + message: + 'Cannot lockdown (repairIntrinsics) because @endo/harden used before lockdown on this stack', + }); +}); diff --git a/packages/harden/test/noop-does-not-freeze-before-lockdown.test.js b/packages/harden/test/noop-does-not-freeze-before-lockdown.test.js new file mode 100644 index 0000000000..68c05460ec --- /dev/null +++ b/packages/harden/test/noop-does-not-freeze-before-lockdown.test.js @@ -0,0 +1,8 @@ +import test from 'ava'; +import harden from '../noop.js'; + +test('harden does not freeze if not locked down', t => { + const object = {}; + harden(object); + t.assert(!Object.isFrozen(object)); +}); diff --git a/packages/harden/test/noop-freezes-after-lockdown.test.js b/packages/harden/test/noop-freezes-after-lockdown.test.js new file mode 100644 index 0000000000..8111adb7ce --- /dev/null +++ b/packages/harden/test/noop-freezes-after-lockdown.test.js @@ -0,0 +1,18 @@ +import 'ses'; +import test from 'ava'; +import harden from '../noop.js'; + +// This test is framed as a stand-alone module because calling `lockdown` has +// side-effects on the realm. + +test('harden freezes object if locked down', t => { + const { isFrozen: preLockdownIsFrozen } = Object; + lockdown(); + const { isFrozen: postLockdownIsFrozen } = Object; + + const object = {}; + harden(object); + + t.true(preLockdownIsFrozen(object)); + t.true(postLockdownIsFrozen(object)); +}); diff --git a/packages/harden/test/shallow-harden.test.js b/packages/harden/test/shallow-harden.test.js new file mode 100644 index 0000000000..b15b4cff3c --- /dev/null +++ b/packages/harden/test/shallow-harden.test.js @@ -0,0 +1,14 @@ +import test from 'ava'; +import harden from '../shallow.js'; + +test('shallow harden hardens properties only', t => { + t.false(Object.isFrozen(Object.prototype)); + const parent = { __proto__: {}, child: {} }; + t.false(Object.isFrozen(parent)); + t.false(Object.isFrozen(parent.child)); + t.false(Object.isFrozen(Object.getPrototypeOf(parent))); + harden(parent); + t.true(Object.isFrozen(parent)); + t.true(Object.isFrozen(parent.child)); + t.false(Object.isFrozen(Object.getPrototypeOf(parent))); +}); From de30ffa26f1c2abc1da570cfe8959c80c70546f9 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 08:08:41 -0700 Subject: [PATCH 17/22] refactor(harden): Copy ses make-hardener.test.js without modification --- packages/harden/test/make-hardener.test.js | 323 +++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 packages/harden/test/make-hardener.test.js diff --git a/packages/harden/test/make-hardener.test.js b/packages/harden/test/make-hardener.test.js new file mode 100644 index 0000000000..71570984f3 --- /dev/null +++ b/packages/harden/test/make-hardener.test.js @@ -0,0 +1,323 @@ +// @ts-nocheck + +import test from 'ava'; +import { makeHardener } from '../src/make-hardener.js'; +import { assert } from '../src/error/assert.js'; + +const { quote: q } = assert; + +test('makeHardener', t => { + const h = makeHardener(); + const o = { a: {} }; + t.is(h(o), o); + t.truthy(Object.isFrozen(o)); + t.truthy(Object.isFrozen(o.a)); +}); + +test('harden the same thing twice', t => { + const h = makeHardener(); + const o = { a: {} }; + t.is(h(o), o); + t.is(h(o), o); + t.truthy(Object.isFrozen(o)); + t.truthy(Object.isFrozen(o.a)); +}); + +test('harden objects with cycles', t => { + const h = makeHardener(); + const o = { a: {} }; + o.a.foo = o; + t.is(h(o), o); + t.truthy(Object.isFrozen(o)); + t.truthy(Object.isFrozen(o.a)); +}); + +test('harden overlapping objects', t => { + const h = makeHardener(); + const o1 = { a: {} }; + const o2 = { a: o1.a }; + t.is(h(o1), o1); + t.truthy(Object.isFrozen(o1)); + t.truthy(Object.isFrozen(o1.a)); + t.falsy(Object.isFrozen(o2)); + t.is(h(o2), o2); + t.truthy(Object.isFrozen(o2)); +}); + +test('harden up prototype chain', t => { + const h = makeHardener(); + const a = { a: 1 }; + const b = { b: 1, __proto__: a }; + const c = { c: 1, __proto__: b }; + + h(c); + t.truthy(Object.isFrozen(a)); +}); + +test('harden tolerates objects with null prototypes', t => { + const h = makeHardener(); + const o = { a: 1 }; + Object.setPrototypeOf(o, null); + t.is(h(o), o); + t.truthy(Object.isFrozen(o)); + t.truthy(Object.isFrozen(o.a)); +}); + +test('harden typed arrays', t => { + const typedArrayConstructors = [ + BigInt64Array, + BigUint64Array, + Float32Array, + Float64Array, + Int16Array, + Int32Array, + Int8Array, + Uint16Array, + Uint32Array, + Uint8Array, + Uint8ClampedArray, + ]; + + for (const TypedArray of typedArrayConstructors) { + const h = makeHardener(); + const a = new TypedArray(1); + + t.is(h(a), a, `harden ${TypedArray}`); + t.truthy(Object.isSealed(a)); + const descriptor = Object.getOwnPropertyDescriptor(a, '0'); + t.is(descriptor.value, a[0]); + // Failed in Node.js 14 and earlier due to an engine bug: + t.is( + descriptor.configurable, + true, + 'hardened typed array indexed property remains configurable', + ); + // Note that indexes of typed arrays are exceptionally writable for hardened objects. + t.is( + descriptor.writable, + true, + 'hardened typed array indexed property is writable', + ); + t.is( + descriptor.enumerable, + true, + 'hardened typed array indexed property is enumerable', + ); + } +}); + +test('harden typed arrays and their expandos', t => { + const h = makeHardener(); + const a = new Uint8Array(1); + const b = new Uint8Array(1); + + // TODO: Use fast-check to generate arbitrary input. + const expandoKeyCandidates = [ + 'x', + 'length', + + // invalid keys + '-1', + '-1.5', + + // number-coercible strings that are not in canonical form + // https://tc39.es/ecma262/#sec-canonicalnumericindexstring + // https://tc39.es/ecma262/#prod-StringNumericLiteral + '', + ' ', + ' 0', + '0\t', + '+0', + '00', + '.0', + '0.', + '0e0', + '0b0', + '0o0', + '0x0', + ' -0', + '-0\t', + '-00', + '-.0', + '-0.', + '-0e0', + '-0b0', + '-0o0', + '-0x0', + '9007199254740993', // reserializes to "9007199254740992" (Number.MAX_SAFE_INTEGER + 1) + '0.0000001', // reserializes to "1e-7" + '1000000000000000000000', // reserializes to "1e+21" + // Exactly one of these is canonical in any given implementation. + // https://tc39.es/ecma262/#sec-numeric-types-number-tostring + '1.2000000000000001', + '1.2000000000000002', + + // Symbols go last because they are returned last. + // https://tc39.es/ecma262/#sec-integer-indexed-exotic-objects-ownpropertykeys + Symbol('unique symbol'), + Symbol.for('registered symbol'), + Symbol('00'), + Symbol.for('00'), + Symbol.match, + ]; + // Test only property keys that are actually supported by the implementation. + const expandoKeys = []; + for (const key of expandoKeyCandidates) { + if (Reflect.defineProperty(b, key, { value: 'test', configurable: true })) { + expandoKeys.push(key); + } + } + for (const key of expandoKeys) { + Object.defineProperty(a, key, { + value: { a: { b: { c: 10 } } }, + enumerable: true, + writable: true, + configurable: true, + }); + } + + t.is(h(a), a, 'harden() must return typed array input'); + t.deepEqual( + Reflect.ownKeys(a), + ['0'].concat(expandoKeys), + 'hardened typed array keys must exactly match pre-hardened keys', + ); + + // Index properties remain writable. + { + const descriptor = Object.getOwnPropertyDescriptor(a, '0'); + t.like( + descriptor, + { value: 0, writable: true, enumerable: true }, + 'hardened typed array index property', + ); + // Failed in Node.js 14 and earlier due to an engine bug: + t.is( + descriptor.configurable, + true, + 'typed array indexed property is configurable', + ); + // Note that indexes of typed arrays are exceptionally writable for hardened objects: + } + + // Non-index properties are locked down. + for (const key of expandoKeys) { + const descriptor = Object.getOwnPropertyDescriptor(a, key); + t.like( + descriptor, + { configurable: false, writable: false, enumerable: true }, + `hardened typed array expando ${q(key)}`, + ); + t.is( + descriptor.value, + a[key], + `hardened typed array expando ${q(key)} value identity`, + ); + t.deepEqual( + descriptor.value, + { a: { b: { c: 10 } } }, + `hardened typed array expando ${q(key)} value shape`, + ); + t.truthy( + Object.isFrozen(descriptor.value), + `hardened typed array expando ${q(key)} value is frozen`, + ); + t.truthy( + Object.isFrozen(descriptor.value.a), + `hardened typed array expando ${q(key)} value property is frozen`, + ); + t.truthy( + Object.isFrozen(descriptor.value.a.b), + `hardened typed array expando ${q(key)} value subproperty is frozen`, + ); + t.truthy( + Object.isFrozen(descriptor.value.a.b.c), + `hardened typed array expando ${q(key)} value sub-subproperty is frozen`, + ); + } + + t.truthy(Object.isSealed(a), 'hardened typed array is sealed'); +}); + +test('hardening makes writable properties readonly even if non-configurable', t => { + const h = makeHardener(); + const o = {}; + Object.defineProperty(o, 'x', { + value: 10, + writable: true, + configurable: false, + enumerable: false, + }); + h(o); + + t.deepEqual(Object.getOwnPropertyDescriptor(o, 'x'), { + value: 10, + writable: false, + configurable: false, + enumerable: false, + }); +}); + +test('harden a typed array with a writable non-configurable expando', t => { + const h = makeHardener(); + const a = new Uint8Array(1); + Object.defineProperty(a, 'x', { + value: 'A', + writable: true, + configurable: false, + enumerable: false, + }); + + t.is(h(a), a); + t.truthy(Object.isSealed(a)); + + t.deepEqual( + { + value: 'A', + writable: false, + configurable: false, + enumerable: false, + }, + Object.getOwnPropertyDescriptor(a, 'x'), + ); +}); + +test('harden a typed array subclass', t => { + const h = makeHardener(); + + class Ooint8Array extends Uint8Array { + oo = 'ghosts'; + } + h(Ooint8Array); + t.truthy(Object.isFrozen(Ooint8Array.prototype)); + t.truthy(Object.isFrozen(Object.getPrototypeOf(Ooint8Array.prototype))); + + const a = new Ooint8Array(1); + t.is(h(a), a); + + t.deepEqual(Object.getOwnPropertyDescriptor(a, 'oo'), { + value: 'ghosts', + writable: false, + configurable: false, + enumerable: true, + }); + t.truthy(Object.isSealed(a)); +}); + +test('harden depends on invariant: typed arrays have no storage for integer indexes beyond length', t => { + const a = new Uint8Array(1); + a[1] = 1; + t.is(a[1], undefined); +}); + +test('harden depends on invariant: typed arrays cannot have integer expandos', t => { + const a = new Uint8Array(1); + t.throws(() => { + Object.defineProperty(a, '1', { + value: 'A', + writable: true, + configurable: false, + enumerable: false, + }); + }); +}); From bd1cca16850c53c005daefadb270883b0b306945 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 08:11:51 -0700 Subject: [PATCH 18/22] refactor(harden): Adapt make-hardener test --- packages/harden/test/make-hardener.test.js | 71 +++++++++++----------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/packages/harden/test/make-hardener.test.js b/packages/harden/test/make-hardener.test.js index 71570984f3..ca3a8abaa9 100644 --- a/packages/harden/test/make-hardener.test.js +++ b/packages/harden/test/make-hardener.test.js @@ -1,66 +1,65 @@ // @ts-nocheck import test from 'ava'; -import { makeHardener } from '../src/make-hardener.js'; -import { assert } from '../src/error/assert.js'; +import { makeHardener } from '../make-hardener.js'; -const { quote: q } = assert; +const { stringify: q } = JSON; test('makeHardener', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const o = { a: {} }; t.is(h(o), o); - t.truthy(Object.isFrozen(o)); - t.truthy(Object.isFrozen(o.a)); + t.true(Object.isFrozen(o)); + t.true(Object.isFrozen(o.a)); }); test('harden the same thing twice', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const o = { a: {} }; t.is(h(o), o); t.is(h(o), o); - t.truthy(Object.isFrozen(o)); - t.truthy(Object.isFrozen(o.a)); + t.true(Object.isFrozen(o)); + t.true(Object.isFrozen(o.a)); }); test('harden objects with cycles', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const o = { a: {} }; o.a.foo = o; t.is(h(o), o); - t.truthy(Object.isFrozen(o)); - t.truthy(Object.isFrozen(o.a)); + t.true(Object.isFrozen(o)); + t.true(Object.isFrozen(o.a)); }); test('harden overlapping objects', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const o1 = { a: {} }; const o2 = { a: o1.a }; t.is(h(o1), o1); - t.truthy(Object.isFrozen(o1)); - t.truthy(Object.isFrozen(o1.a)); + t.true(Object.isFrozen(o1)); + t.true(Object.isFrozen(o1.a)); t.falsy(Object.isFrozen(o2)); t.is(h(o2), o2); - t.truthy(Object.isFrozen(o2)); + t.true(Object.isFrozen(o2)); }); test('harden up prototype chain', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const a = { a: 1 }; const b = { b: 1, __proto__: a }; const c = { c: 1, __proto__: b }; h(c); - t.truthy(Object.isFrozen(a)); + t.true(Object.isFrozen(a)); }); test('harden tolerates objects with null prototypes', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const o = { a: 1 }; Object.setPrototypeOf(o, null); t.is(h(o), o); - t.truthy(Object.isFrozen(o)); - t.truthy(Object.isFrozen(o.a)); + t.true(Object.isFrozen(o)); + t.true(Object.isFrozen(o.a)); }); test('harden typed arrays', t => { @@ -79,11 +78,11 @@ test('harden typed arrays', t => { ]; for (const TypedArray of typedArrayConstructors) { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const a = new TypedArray(1); t.is(h(a), a, `harden ${TypedArray}`); - t.truthy(Object.isSealed(a)); + t.true(Object.isSealed(a)); const descriptor = Object.getOwnPropertyDescriptor(a, '0'); t.is(descriptor.value, a[0]); // Failed in Node.js 14 and earlier due to an engine bug: @@ -107,7 +106,7 @@ test('harden typed arrays', t => { }); test('harden typed arrays and their expandos', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const a = new Uint8Array(1); const b = new Uint8Array(1); @@ -218,29 +217,29 @@ test('harden typed arrays and their expandos', t => { { a: { b: { c: 10 } } }, `hardened typed array expando ${q(key)} value shape`, ); - t.truthy( + t.true( Object.isFrozen(descriptor.value), `hardened typed array expando ${q(key)} value is frozen`, ); - t.truthy( + t.true( Object.isFrozen(descriptor.value.a), `hardened typed array expando ${q(key)} value property is frozen`, ); - t.truthy( + t.true( Object.isFrozen(descriptor.value.a.b), `hardened typed array expando ${q(key)} value subproperty is frozen`, ); - t.truthy( + t.true( Object.isFrozen(descriptor.value.a.b.c), `hardened typed array expando ${q(key)} value sub-subproperty is frozen`, ); } - t.truthy(Object.isSealed(a), 'hardened typed array is sealed'); + t.true(Object.isSealed(a), 'hardened typed array is sealed'); }); test('hardening makes writable properties readonly even if non-configurable', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const o = {}; Object.defineProperty(o, 'x', { value: 10, @@ -259,7 +258,7 @@ test('hardening makes writable properties readonly even if non-configurable', t }); test('harden a typed array with a writable non-configurable expando', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); const a = new Uint8Array(1); Object.defineProperty(a, 'x', { value: 'A', @@ -269,7 +268,7 @@ test('harden a typed array with a writable non-configurable expando', t => { }); t.is(h(a), a); - t.truthy(Object.isSealed(a)); + t.true(Object.isSealed(a)); t.deepEqual( { @@ -283,14 +282,14 @@ test('harden a typed array with a writable non-configurable expando', t => { }); test('harden a typed array subclass', t => { - const h = makeHardener(); + const h = makeHardener({ traversePrototypes: true }); class Ooint8Array extends Uint8Array { oo = 'ghosts'; } h(Ooint8Array); - t.truthy(Object.isFrozen(Ooint8Array.prototype)); - t.truthy(Object.isFrozen(Object.getPrototypeOf(Ooint8Array.prototype))); + t.true(Object.isFrozen(Ooint8Array.prototype)); + t.true(Object.isFrozen(Object.getPrototypeOf(Ooint8Array.prototype))); const a = new Ooint8Array(1); t.is(h(a), a); @@ -301,7 +300,7 @@ test('harden a typed array subclass', t => { configurable: false, enumerable: true, }); - t.truthy(Object.isSealed(a)); + t.true(Object.isSealed(a)); }); test('harden depends on invariant: typed arrays have no storage for integer indexes beyond length', t => { From bc2ad3ea0a498d52752fc0ddd69acf2ce142037a Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 08:13:29 -0700 Subject: [PATCH 19/22] refactor(harden): Duplicate make-hardener test for shallow --- .../harden/test/make-hardener-shallow.test.js | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 packages/harden/test/make-hardener-shallow.test.js diff --git a/packages/harden/test/make-hardener-shallow.test.js b/packages/harden/test/make-hardener-shallow.test.js new file mode 100644 index 0000000000..ca3a8abaa9 --- /dev/null +++ b/packages/harden/test/make-hardener-shallow.test.js @@ -0,0 +1,322 @@ +// @ts-nocheck + +import test from 'ava'; +import { makeHardener } from '../make-hardener.js'; + +const { stringify: q } = JSON; + +test('makeHardener', t => { + const h = makeHardener({ traversePrototypes: true }); + const o = { a: {} }; + t.is(h(o), o); + t.true(Object.isFrozen(o)); + t.true(Object.isFrozen(o.a)); +}); + +test('harden the same thing twice', t => { + const h = makeHardener({ traversePrototypes: true }); + const o = { a: {} }; + t.is(h(o), o); + t.is(h(o), o); + t.true(Object.isFrozen(o)); + t.true(Object.isFrozen(o.a)); +}); + +test('harden objects with cycles', t => { + const h = makeHardener({ traversePrototypes: true }); + const o = { a: {} }; + o.a.foo = o; + t.is(h(o), o); + t.true(Object.isFrozen(o)); + t.true(Object.isFrozen(o.a)); +}); + +test('harden overlapping objects', t => { + const h = makeHardener({ traversePrototypes: true }); + const o1 = { a: {} }; + const o2 = { a: o1.a }; + t.is(h(o1), o1); + t.true(Object.isFrozen(o1)); + t.true(Object.isFrozen(o1.a)); + t.falsy(Object.isFrozen(o2)); + t.is(h(o2), o2); + t.true(Object.isFrozen(o2)); +}); + +test('harden up prototype chain', t => { + const h = makeHardener({ traversePrototypes: true }); + const a = { a: 1 }; + const b = { b: 1, __proto__: a }; + const c = { c: 1, __proto__: b }; + + h(c); + t.true(Object.isFrozen(a)); +}); + +test('harden tolerates objects with null prototypes', t => { + const h = makeHardener({ traversePrototypes: true }); + const o = { a: 1 }; + Object.setPrototypeOf(o, null); + t.is(h(o), o); + t.true(Object.isFrozen(o)); + t.true(Object.isFrozen(o.a)); +}); + +test('harden typed arrays', t => { + const typedArrayConstructors = [ + BigInt64Array, + BigUint64Array, + Float32Array, + Float64Array, + Int16Array, + Int32Array, + Int8Array, + Uint16Array, + Uint32Array, + Uint8Array, + Uint8ClampedArray, + ]; + + for (const TypedArray of typedArrayConstructors) { + const h = makeHardener({ traversePrototypes: true }); + const a = new TypedArray(1); + + t.is(h(a), a, `harden ${TypedArray}`); + t.true(Object.isSealed(a)); + const descriptor = Object.getOwnPropertyDescriptor(a, '0'); + t.is(descriptor.value, a[0]); + // Failed in Node.js 14 and earlier due to an engine bug: + t.is( + descriptor.configurable, + true, + 'hardened typed array indexed property remains configurable', + ); + // Note that indexes of typed arrays are exceptionally writable for hardened objects. + t.is( + descriptor.writable, + true, + 'hardened typed array indexed property is writable', + ); + t.is( + descriptor.enumerable, + true, + 'hardened typed array indexed property is enumerable', + ); + } +}); + +test('harden typed arrays and their expandos', t => { + const h = makeHardener({ traversePrototypes: true }); + const a = new Uint8Array(1); + const b = new Uint8Array(1); + + // TODO: Use fast-check to generate arbitrary input. + const expandoKeyCandidates = [ + 'x', + 'length', + + // invalid keys + '-1', + '-1.5', + + // number-coercible strings that are not in canonical form + // https://tc39.es/ecma262/#sec-canonicalnumericindexstring + // https://tc39.es/ecma262/#prod-StringNumericLiteral + '', + ' ', + ' 0', + '0\t', + '+0', + '00', + '.0', + '0.', + '0e0', + '0b0', + '0o0', + '0x0', + ' -0', + '-0\t', + '-00', + '-.0', + '-0.', + '-0e0', + '-0b0', + '-0o0', + '-0x0', + '9007199254740993', // reserializes to "9007199254740992" (Number.MAX_SAFE_INTEGER + 1) + '0.0000001', // reserializes to "1e-7" + '1000000000000000000000', // reserializes to "1e+21" + // Exactly one of these is canonical in any given implementation. + // https://tc39.es/ecma262/#sec-numeric-types-number-tostring + '1.2000000000000001', + '1.2000000000000002', + + // Symbols go last because they are returned last. + // https://tc39.es/ecma262/#sec-integer-indexed-exotic-objects-ownpropertykeys + Symbol('unique symbol'), + Symbol.for('registered symbol'), + Symbol('00'), + Symbol.for('00'), + Symbol.match, + ]; + // Test only property keys that are actually supported by the implementation. + const expandoKeys = []; + for (const key of expandoKeyCandidates) { + if (Reflect.defineProperty(b, key, { value: 'test', configurable: true })) { + expandoKeys.push(key); + } + } + for (const key of expandoKeys) { + Object.defineProperty(a, key, { + value: { a: { b: { c: 10 } } }, + enumerable: true, + writable: true, + configurable: true, + }); + } + + t.is(h(a), a, 'harden() must return typed array input'); + t.deepEqual( + Reflect.ownKeys(a), + ['0'].concat(expandoKeys), + 'hardened typed array keys must exactly match pre-hardened keys', + ); + + // Index properties remain writable. + { + const descriptor = Object.getOwnPropertyDescriptor(a, '0'); + t.like( + descriptor, + { value: 0, writable: true, enumerable: true }, + 'hardened typed array index property', + ); + // Failed in Node.js 14 and earlier due to an engine bug: + t.is( + descriptor.configurable, + true, + 'typed array indexed property is configurable', + ); + // Note that indexes of typed arrays are exceptionally writable for hardened objects: + } + + // Non-index properties are locked down. + for (const key of expandoKeys) { + const descriptor = Object.getOwnPropertyDescriptor(a, key); + t.like( + descriptor, + { configurable: false, writable: false, enumerable: true }, + `hardened typed array expando ${q(key)}`, + ); + t.is( + descriptor.value, + a[key], + `hardened typed array expando ${q(key)} value identity`, + ); + t.deepEqual( + descriptor.value, + { a: { b: { c: 10 } } }, + `hardened typed array expando ${q(key)} value shape`, + ); + t.true( + Object.isFrozen(descriptor.value), + `hardened typed array expando ${q(key)} value is frozen`, + ); + t.true( + Object.isFrozen(descriptor.value.a), + `hardened typed array expando ${q(key)} value property is frozen`, + ); + t.true( + Object.isFrozen(descriptor.value.a.b), + `hardened typed array expando ${q(key)} value subproperty is frozen`, + ); + t.true( + Object.isFrozen(descriptor.value.a.b.c), + `hardened typed array expando ${q(key)} value sub-subproperty is frozen`, + ); + } + + t.true(Object.isSealed(a), 'hardened typed array is sealed'); +}); + +test('hardening makes writable properties readonly even if non-configurable', t => { + const h = makeHardener({ traversePrototypes: true }); + const o = {}; + Object.defineProperty(o, 'x', { + value: 10, + writable: true, + configurable: false, + enumerable: false, + }); + h(o); + + t.deepEqual(Object.getOwnPropertyDescriptor(o, 'x'), { + value: 10, + writable: false, + configurable: false, + enumerable: false, + }); +}); + +test('harden a typed array with a writable non-configurable expando', t => { + const h = makeHardener({ traversePrototypes: true }); + const a = new Uint8Array(1); + Object.defineProperty(a, 'x', { + value: 'A', + writable: true, + configurable: false, + enumerable: false, + }); + + t.is(h(a), a); + t.true(Object.isSealed(a)); + + t.deepEqual( + { + value: 'A', + writable: false, + configurable: false, + enumerable: false, + }, + Object.getOwnPropertyDescriptor(a, 'x'), + ); +}); + +test('harden a typed array subclass', t => { + const h = makeHardener({ traversePrototypes: true }); + + class Ooint8Array extends Uint8Array { + oo = 'ghosts'; + } + h(Ooint8Array); + t.true(Object.isFrozen(Ooint8Array.prototype)); + t.true(Object.isFrozen(Object.getPrototypeOf(Ooint8Array.prototype))); + + const a = new Ooint8Array(1); + t.is(h(a), a); + + t.deepEqual(Object.getOwnPropertyDescriptor(a, 'oo'), { + value: 'ghosts', + writable: false, + configurable: false, + enumerable: true, + }); + t.true(Object.isSealed(a)); +}); + +test('harden depends on invariant: typed arrays have no storage for integer indexes beyond length', t => { + const a = new Uint8Array(1); + a[1] = 1; + t.is(a[1], undefined); +}); + +test('harden depends on invariant: typed arrays cannot have integer expandos', t => { + const a = new Uint8Array(1); + t.throws(() => { + Object.defineProperty(a, '1', { + value: 'A', + writable: true, + configurable: false, + enumerable: false, + }); + }); +}); From 9432f38da21731069aa7df358d5599b44a624cba Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 08:14:54 -0700 Subject: [PATCH 20/22] refactor(harden): Adapt shallow make-hardener test --- .../harden/test/make-hardener-shallow.test.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/harden/test/make-hardener-shallow.test.js b/packages/harden/test/make-hardener-shallow.test.js index ca3a8abaa9..774c3eab16 100644 --- a/packages/harden/test/make-hardener-shallow.test.js +++ b/packages/harden/test/make-hardener-shallow.test.js @@ -6,7 +6,7 @@ import { makeHardener } from '../make-hardener.js'; const { stringify: q } = JSON; test('makeHardener', t => { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); const o = { a: {} }; t.is(h(o), o); t.true(Object.isFrozen(o)); @@ -14,7 +14,7 @@ test('makeHardener', t => { }); test('harden the same thing twice', t => { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); const o = { a: {} }; t.is(h(o), o); t.is(h(o), o); @@ -23,7 +23,7 @@ test('harden the same thing twice', t => { }); test('harden objects with cycles', t => { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); const o = { a: {} }; o.a.foo = o; t.is(h(o), o); @@ -32,7 +32,7 @@ test('harden objects with cycles', t => { }); test('harden overlapping objects', t => { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); const o1 = { a: {} }; const o2 = { a: o1.a }; t.is(h(o1), o1); @@ -43,18 +43,18 @@ test('harden overlapping objects', t => { t.true(Object.isFrozen(o2)); }); -test('harden up prototype chain', t => { - const h = makeHardener({ traversePrototypes: true }); +test('do not harden up prototype chain', t => { + const h = makeHardener({ traversePrototypes: false }); const a = { a: 1 }; const b = { b: 1, __proto__: a }; const c = { c: 1, __proto__: b }; h(c); - t.true(Object.isFrozen(a)); + t.false(Object.isFrozen(a)); }); test('harden tolerates objects with null prototypes', t => { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); const o = { a: 1 }; Object.setPrototypeOf(o, null); t.is(h(o), o); @@ -78,7 +78,7 @@ test('harden typed arrays', t => { ]; for (const TypedArray of typedArrayConstructors) { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); const a = new TypedArray(1); t.is(h(a), a, `harden ${TypedArray}`); @@ -106,7 +106,7 @@ test('harden typed arrays', t => { }); test('harden typed arrays and their expandos', t => { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); const a = new Uint8Array(1); const b = new Uint8Array(1); @@ -239,7 +239,7 @@ test('harden typed arrays and their expandos', t => { }); test('hardening makes writable properties readonly even if non-configurable', t => { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); const o = {}; Object.defineProperty(o, 'x', { value: 10, @@ -258,7 +258,7 @@ test('hardening makes writable properties readonly even if non-configurable', t }); test('harden a typed array with a writable non-configurable expando', t => { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); const a = new Uint8Array(1); Object.defineProperty(a, 'x', { value: 'A', @@ -282,14 +282,14 @@ test('harden a typed array with a writable non-configurable expando', t => { }); test('harden a typed array subclass', t => { - const h = makeHardener({ traversePrototypes: true }); + const h = makeHardener({ traversePrototypes: false }); class Ooint8Array extends Uint8Array { oo = 'ghosts'; } h(Ooint8Array); t.true(Object.isFrozen(Ooint8Array.prototype)); - t.true(Object.isFrozen(Object.getPrototypeOf(Ooint8Array.prototype))); + t.false(Object.isFrozen(Object.getPrototypeOf(Ooint8Array.prototype))); const a = new Ooint8Array(1); t.is(h(a), a); From 5d375cc471c90528abe20b26b73b74e2f80cafd1 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 30 Sep 2025 23:26:07 -0700 Subject: [PATCH 21/22] chore: Update yarn.lock --- yarn.lock | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/yarn.lock b/yarn.lock index 2fe344d8ea..d0160a8336 100644 --- a/yarn.lock +++ b/yarn.lock @@ -463,6 +463,19 @@ __metadata: languageName: unknown linkType: soft +"@endo/harden@workspace:packages/harden": + version: 0.0.0-use.local + resolution: "@endo/harden@workspace:packages/harden" + dependencies: + ava: "catalog:dev" + c8: "catalog:dev" + eslint: "catalog:dev" + ses: "workspace:^" + tsd: "catalog:dev" + typescript: "npm:~5.9.2" + languageName: unknown + linkType: soft + "@endo/immutable-arraybuffer@workspace:^, @endo/immutable-arraybuffer@workspace:packages/immutable-arraybuffer": version: 0.0.0-use.local resolution: "@endo/immutable-arraybuffer@workspace:packages/immutable-arraybuffer" From d52dcb9b0330aed2df6c760bdd62bc8a15345eaa Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 10 Oct 2025 07:09:49 -0700 Subject: [PATCH 22/22] doc(harden): README --- packages/harden/README.md | 155 +++++++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/packages/harden/README.md b/packages/harden/README.md index 303683fd0e..f3e45675ed 100644 --- a/packages/harden/README.md +++ b/packages/harden/README.md @@ -1,3 +1,156 @@ # harden -This `@endo/harden` package is a skeleton package. +Hardened modules are modules that make their exports resist tampering by other +modules that import them, making them less suceptible to supply chain attack. +In [HardenedJS](https://hardenedjs.org), the global `harden` function +transitively freezes an object and all of the objects that are reachable by +walking up chains of properties and prototypes. +All the primordials like `Array.prototype` and `Object` are frozen in +this environment, which gives your module a place to stand toward its own +defense. +Then, with [LavaMoat](https://github.com/lavamoat/lavamoat), each package +is credibly isolated and only receives the subset of globals and host modules +it needs to function. +That is, we can enforce [Principle of Least +Authority](https://en.wikipedia.org/wiki/Principle_of_least_privilege). +But, that leaves the module to use `harden` to freeze all its exports and +anything it returns that might be shared by other packages that use it. + +In order to provide type information about the global `harden` in lockded-down +HardenedJS, and also to make it possible for hardened modules to be used +outside HardenedJS, the `@endo/harden` package exports a `harden` function that +can be used either way. + +```js +import { harden } from '@endo/harden'; + +export const myFunction = () => {}; +harden(myFunction); +``` + +By avoiding the export of hoisted `function` and `var` declarations and by +immediately calling `harden` on any exposed function (or prototype thereof!) we +leave no window of opportunity for another module to alter our exports. +If a function's return value is meant to be shared by multiple parties (such +as memoized objects), a hardened module author should harden the value before +the function returns it (`return harden(value);`). + +# With HardenedJS + +The package `@endo/harden` reexports the `globalThis.harden` or +`Object[Symbol.for('harden')]` in its execution environment, in order of +preference, and is suitable regardless of whether a module is used +with or without HardenedJS. + +When using SES, `lockdown` creates `globalThis.harden` in the Realm's +intrinsic `globalThis` and also automatically endows `globalThis.harden` +to any `Compartment`. +It is possible to delete `globalThis.harden` on new compartments. +However, every version of SES published since the introduction of `@endo/harden` +also provides `Object[Symbol.for('harden')]`, which is a property of one +of the hardened shared intrinsics and cannot be subverted in a compartment. + +The `harden` in `@endo/harden` prefers `globalThis.harden` because this +affords the greatest degree of flexibility. +Any multi-tenant `Compartment` should freeze its own `globalThis`, including +making `harden` non-configurable and non-writable, so there is no risk +of tampering, and endowing a `Compartment` with a different `harden` +than the Realm's `Object[Symbol.for('harden')]` may be useful for some +cases. + +When creating a bundle for an application that can safely assume it will run in +a HardenedJS environment, consider passing the build condition `-C hardened`. +This will provide the smallest version of `@endo/harden`, one which will throw +an exception if `harden` is not present. + +``` +bundle-source -C hardened entry.js > entry.json +``` + +# Without HardenedJS + +Libraries that use `@endo/harden` can be used without HardenedJS and the +exported `harden` only freezes the transitive owned properties of the object +and does not traverse prototype chains. + +Consequently, the surface of an object is immutable. +However, if any fields of an object are optional, an attacker can subvert them +by altering their prototype. +This provides a degree of immutability that is useful for partial safety and +does not interfere with uncoordinated alteration of the realm intrinsics, on +which some testing and frontend user interface frameworks rely. + +To opt out of any safety guarantees and to avoid the computation cost of +transitively hardening own properties, use the `-C harden:unsafe` build +condition with tools like `node` and Endo's `bundle-source`. + +# Multiple instances + +The first instance of `@endo/harden` will determine the behavior of any +subsequent instance of `@endo/harden` that initializes later, regardless of +differences in behavior. +In a mutable, pre-lockdown JavaScript environment, it does this by behaving +somewhat like a shim. +A side-effect of the _first use_ of `harden` is that it installs its flavor of +`harden` at `Object[Symbol.for('harden')]` and all subsequent initializations +just adopt that behavior. +This property is how `lockdown` senses that it should fail. + +# With _or_ Without _not_ Both + +Hardened modules calling `harden` should be fine at any time in an application +that never uses HardenedJS, calling `lockdown`. + +However, initializing a hardened module before setting up a HardenedJS +environment (before calling `lockdown`) and then proceeding on the assumption +that it's hardened after `lockdown` would leave the apparently-hardened module +vulnerable. + +So, `@endo/harden` arranges for `lockdown()` to throw an exception with +a _helpful_ stack if `harden` gets called before `lockdown`. +The stack points to the module that was initialized before `lockdown` +and which should be moved after `lockdown`. +The `lockdown` call often occurs as a side-effect of initializing +`@endo/lockdown`, `@endo/init`, or by convention, modules with names like +`prepare-*`. + +# Configurability of Compartment harden + +The `harden` exported by `@endo/harden` prefers `Object[Symbol.for('harden')]` +over `globalThis.harden` since the former is an intrinsic that cannot be +overridden by an endowment. +Any code that relies on `globalThis.harden` being endowed with a different +behavior than `Object[Symbol.for('harden')]` cannot substitute `@endo/harden`. + +# isFake + +Using `lockdown` with the `"unsafe"` `hardenTaming` option creates an environment +where `Object.isFrozen`, `Object.isExtensible`, `Reflect.isExtensible`, and +`isSealed` all misreport that any object is frozen, non-extensible, and sealed. +To indicate this, `harden.isFake` is `true`. + +We regret this misfeature. +The `@endo/harden` does not provide `harden.isFake`. +Code, especially tests, migrating to use `@endo/harden` should refactor +`harden.isFake` to use a more legible indicator of the misbehavior of `isFrozen` +and its compatriots, which may not be indicated by empirical behavior of `harden`. + +For example, `Object.isFrozen({})` when `harden.isFake` and more clearly +conveys the reason a test might be invalidated by `unsafe` `hardenTaming`. +Testing that the outcome of `Object.isFrozen({})` is the same as the outcome of +`Object.isFrozen(object)` for an object that should not be frozen makes a test +work just as well between `safe` and `unsafe` `hardenTaming`. + +The module `@endo/harden/is-noop.js` provides `hardenIsNoop(harden)` to +indicate that `harden` is a no-op, regardless of `hardenTaming`. +Do not rely on `Object.isFrozen({})` to imply that `harden` is a no-op. + +```js +import harden from '@endo/harden'; +import hardenIsNoop from '@endo/harden-is-noop.js'; + +if (hardenIsNoop(harden)) { + // ... +} +``` +