diff options
author | Guy Bedford <guybedford@gmail.com> | 2019-01-24 20:56:33 +0200 |
---|---|---|
committer | Ruben Bridgewater <ruben@bridgewater.de> | 2019-03-14 17:15:27 +0100 |
commit | 3e54f909118bdf763aee6f0ae11643d37d5eb0b9 (patch) | |
tree | 26439509f3d32c66f18fee78f14fa38e406606dc | |
parent | 1c8076ef58863bfd966a6b66c2b31939cc603a0f (diff) | |
download | node-new-3e54f909118bdf763aee6f0ae11643d37d5eb0b9.tar.gz |
bootstrap: experimental --frozen-intrinsics flag
PR-URL: https://github.com/nodejs/node/pull/25685
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
-rw-r--r-- | LICENSE | 18 | ||||
-rw-r--r-- | doc/api/cli.md | 20 | ||||
-rw-r--r-- | doc/node.1 | 3 | ||||
-rw-r--r-- | lib/internal/bootstrap/pre_execution.js | 10 | ||||
-rw-r--r-- | lib/internal/freeze_intrinsics.js | 244 | ||||
-rw-r--r-- | lib/internal/main/worker_thread.js | 2 | ||||
-rw-r--r-- | node.gyp | 1 | ||||
-rw-r--r-- | src/node_options.cc | 4 | ||||
-rw-r--r-- | src/node_options.h | 1 | ||||
-rw-r--r-- | test/parallel/test-freeze-intrinsics.js | 10 | ||||
-rwxr-xr-x | tools/license-builder.sh | 3 |
11 files changed, 316 insertions, 0 deletions
@@ -1360,6 +1360,24 @@ The externally maintained libraries used by Node.js are: OR OTHER DEALINGS IN THE SOFTWARE. """ +- caja, located at lib/internal/freeze_intrinsics.js, is licensed as follows: + """ + 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. + """ + - brotli, located at deps/brotli, is licensed as follows: """ Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. diff --git a/doc/api/cli.md b/doc/api/cli.md index 71e36a4c12..831b9624d1 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -177,6 +177,25 @@ added: v6.0.0 Force FIPS-compliant crypto on startup. (Cannot be disabled from script code.) (Same requirements as `--enable-fips`.) +### `--frozen-intrinsics` +<!-- YAML +added: REPLACEME +--> + +> Stability: 1 - Experimental + +Enable experimental frozen intrinsics like `Array` and `Object`. + +Support is currently only provided for the root context and no guarantees are +currently provided that `global.Array` is indeed the default intrinsic +reference. + +**Code breakage is highly likely with this flag**, especially since limited +support for subclassing builtins is provided currently due to ECMA-262 bug +https://github.com/tc39/ecma262/pull/1320. + +Both of the above may change in future updates, which will be breaking changes. + ### `--http-parser=library` <!-- YAML added: v11.4.0 @@ -671,6 +690,7 @@ Node.js options that are allowed are: - `--experimental-report` - `--experimental-vm-modules` - `--force-fips` +- `--frozen-intrinsics` - `--icu-data-dir` - `--inspect` - `--inspect-brk` diff --git a/doc/node.1 b/doc/node.1 index c0bdeb1beb..603d72d566 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -144,6 +144,9 @@ Force FIPS-compliant crypto on startup Same requirements as .Fl -enable-fips . . +.It Fl -frozen-intrinsics +Enable experimental frozen intrinsics support. +. .It Fl -http-parser Ns = Ns Ar library Chooses an HTTP parser library. Available values are .Sy llhttp diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js index 3e64540aa3..02acf6b46f 100644 --- a/lib/internal/bootstrap/pre_execution.js +++ b/lib/internal/bootstrap/pre_execution.js @@ -31,6 +31,7 @@ function prepareMainThreadExecution() { initializeClusterIPC(); initializeDeprecations(); + initializeFrozenIntrinsics(); initializeESMLoader(); loadPreloadModules(); } @@ -230,6 +231,14 @@ function initializeESMLoader() { } } +function initializeFrozenIntrinsics() { + if (getOptionValue('--frozen-intrinsics')) { + process.emitWarning('The --frozen-intrinsics flag is experimental', + 'ExperimentalWarning'); + require('internal/freeze_intrinsics')(); + } +} + function loadPreloadModules() { // For user code, we preload modules if `-r` is passed const preloadModules = getOptionValue('--require'); @@ -245,6 +254,7 @@ module.exports = { prepareMainThreadExecution, initializeDeprecations, initializeESMLoader, + initializeFrozenIntrinsics, loadPreloadModules, setupTraceCategoryState, initializeReport diff --git a/lib/internal/freeze_intrinsics.js b/lib/internal/freeze_intrinsics.js new file mode 100644 index 0000000000..effac31853 --- /dev/null +++ b/lib/internal/freeze_intrinsics.js @@ -0,0 +1,244 @@ +// 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. +// SPDX-License-Identifier: MIT + +// 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 +// https://github.com/tc39/proposal-frozen-realms/blob/91ac390e3451da92b5c27e354b39e52b7636a437/shim/src/deep-freeze.js + +/* global WebAssembly, SharedArrayBuffer, console */ +'use strict'; +module.exports = function() { + + const intrinsics = [ + // Anonymous Intrinsics + // ThrowTypeError + Object.getOwnPropertyDescriptor(Function.prototype, 'caller').get, + // IteratorPrototype + Object.getPrototypeOf( + Object.getPrototypeOf(new Array()[Symbol.iterator]()) + ), + // ArrayIteratorPrototype + Object.getPrototypeOf(new Array()[Symbol.iterator]()), + // StringIteratorPrototype + Object.getPrototypeOf(new String()[Symbol.iterator]()), + // MapIteratorPrototype + Object.getPrototypeOf(new Map()[Symbol.iterator]()), + // SetIteratorPrototype + Object.getPrototypeOf(new Set()[Symbol.iterator]()), + // GeneratorFunction + Object.getPrototypeOf(function* () {}), + // AsyncFunction + Object.getPrototypeOf(async function() {}), + // AsyncGeneratorFunction + Object.getPrototypeOf(async function* () {}), + // TypedArray + Object.getPrototypeOf(Uint8Array), + + // 18 The Global Object + eval, + isFinite, + isNaN, + parseFloat, + parseInt, + decodeURI, + decodeURIComponent, + encodeURI, + encodeURIComponent, + + // 19 Fundamental Objects + Object, // 19.1 + Function, // 19.2 + Boolean, // 19.3 + Symbol, // 19.4 + + // Disabled pending stack trace mutation handling + // Error, // 19.5 + // EvalError, + // RangeError, + // ReferenceError, + // SyntaxError, + // TypeError, + // URIError, + + // 20 Numbers and Dates + Number, // 20.1 + Math, // 20.2 + Date, // 20.3 + + // 21 Text Processing + String, // 21.1 + RegExp, // 21.2 + + // 22 Indexed Collections + Array, // 22.1 + + Int8Array, + Uint8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + BigInt64Array, + BigUint64Array, + + // 23 Keyed Collections + Map, // 23.1 + Set, // 23.2 + WeakMap, // 23.3 + WeakSet, // 23.4 + + // 24 Structured Data + ArrayBuffer, // 24.1 + DataView, // 24.3 + JSON, // 24.5 + Promise, // 25.4 + + // 26 Reflection + Reflect, // 26.1 + Proxy, // 26.2 + + // B.2.1 + escape, + unescape, + + // Web compatibility + clearImmediate, + clearInterval, + clearTimeout, + decodeURI, + decodeURIComponent, + encodeURI, + encodeURIComponent, + setImmediate, + setInterval, + setTimeout, + + // Other APIs + console, + BigInt, + Atomics, + WebAssembly, + SharedArrayBuffer + ]; + + if (typeof Intl !== 'undefined') + intrinsics.push(Intl); + + intrinsics.forEach(deepFreeze); + + function deepFreeze(root) { + + const { freeze, getOwnPropertyDescriptors, getPrototypeOf } = Object; + const { ownKeys } = Reflect; + + // Objects that are deeply frozen. + // It turns out that Error is reachable from WebAssembly so it is + // explicitly added here to ensure it is not frozen + const frozenSet = new WeakSet([Error, Error.prototype]); + + /** + * "innerDeepFreeze()" acts like "Object.freeze()", except that: + * + * To deepFreeze an object is to freeze it and all objects transitively + * reachable from it via transitive reflective property and prototype + * traversal. + */ + function innerDeepFreeze(node) { + // Objects that we have frozen in this round. + const freezingSet = new Set(); + + // If val is something we should be freezing but aren't yet, + // add it to freezingSet. + function enqueue(val) { + if (Object(val) !== val) { + // ignore primitives + return; + } + const type = typeof val; + if (type !== 'object' && type !== 'function') { + // NB: handle for any new cases in future + } + if (frozenSet.has(val) || freezingSet.has(val)) { + // todo use uncurried form + // Ignore if already frozen or freezing + return; + } + freezingSet.add(val); // todo use uncurried form + } + + function doFreeze(obj) { + // Immediately 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). + 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 proto = getPrototypeOf(obj); + const descs = getOwnPropertyDescriptors(obj); + enqueue(proto); + ownKeys(descs).forEach((name) => { + // todo uncurried form + // todo: 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. + const desc = descs[name]; + if ('value' in desc) { + // todo uncurried form + enqueue(desc.value); + } else { + enqueue(desc.get); + enqueue(desc.set); + } + }); + } + + function dequeue() { + // New values added before forEach() has finished will be visited. + freezingSet.forEach(doFreeze); // todo curried forEach + } + + function commit() { + // todo curried forEach + // we capture the real WeakSet.prototype.add above, in case someone + // changes it. The two-argument form of forEach passes the second + // argument as the 'this' binding, so we add to the correct set. + freezingSet.forEach(frozenSet.add, frozenSet); + } + + enqueue(node); + dequeue(); + commit(); + } + + innerDeepFreeze(root); + return root; + } +}; diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index b6dbc47274..0dc1b61ef9 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -6,6 +6,7 @@ const { initializeDeprecations, initializeESMLoader, + initializeFrozenIntrinsics, initializeReport, loadPreloadModules, setupTraceCategoryState @@ -81,6 +82,7 @@ port.on('message', (message) => { require('internal/process/policy').setup(manifestSrc, manifestURL); } initializeDeprecations(); + initializeFrozenIntrinsics(); initializeESMLoader(); loadPreloadModules(); publicWorker.parentPort = publicPort; @@ -123,6 +123,7 @@ 'lib/internal/error-serdes.js', 'lib/internal/fixed_queue.js', 'lib/internal/freelist.js', + 'lib/internal/freeze_intrinsics.js', 'lib/internal/fs/promises.js', 'lib/internal/fs/read_file_context.js', 'lib/internal/fs/streams.js', diff --git a/src/node_options.cc b/src/node_options.cc index ebd4baf2b7..9f94c75767 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -187,6 +187,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { kAllowedInEnvironment); #endif // NODE_REPORT AddOption("--expose-internals", "", &EnvironmentOptions::expose_internals); + AddOption("--frozen-intrinsics", + "experimental frozen intrinsics support", + &EnvironmentOptions::frozen_intrinsics, + kAllowedInEnvironment); AddOption("--http-parser", "Select which HTTP parser to use; either 'legacy' or 'llhttp' " #ifdef NODE_EXPERIMENTAL_HTTP_DEFAULT diff --git a/src/node_options.h b/src/node_options.h index 46963181aa..b3ac910b3d 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -100,6 +100,7 @@ class EnvironmentOptions : public Options { bool experimental_repl_await = false; bool experimental_vm_modules = false; bool expose_internals = false; + bool frozen_intrinsics = false; std::string http_parser = #ifdef NODE_EXPERIMENTAL_HTTP_DEFAULT "llhttp"; diff --git a/test/parallel/test-freeze-intrinsics.js b/test/parallel/test-freeze-intrinsics.js new file mode 100644 index 0000000000..bb94946d2d --- /dev/null +++ b/test/parallel/test-freeze-intrinsics.js @@ -0,0 +1,10 @@ +// Flags: --frozen-intrinsics +'use strict'; +require('../common'); +const assert = require('assert'); + +try { + Object.defineProperty = 'asdf'; + assert(false); +} catch { +} diff --git a/tools/license-builder.sh b/tools/license-builder.sh index 1e82d9da0d..55f630285e 100755 --- a/tools/license-builder.sh +++ b/tools/license-builder.sh @@ -94,6 +94,9 @@ addlicense "node-inspect" "deps/node-inspect" "$(cat ${rootdir}/deps/node-inspec # large_pages addlicense "large_pages" "src/large_pages" "$(sed -e '/SPDX-License-Identifier/,$d' -e 's/^\/\///' ${rootdir}/src/large_pages/node_large_page.h)" +# deep_freeze +addlicense "caja" "lib/internal/freeze_intrinsics.js" "$(sed -e '/SPDX-License-Identifier/,$d' -e 's/^\/\///' ${rootdir}/lib/internal/freeze_intrinsics.js)" + # brotli addlicense "brotli" "deps/brotli" "$(cat ${rootdir}/deps/brotli/LICENSE)" |