Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions doc/contributing/primordials.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,65 @@ There are some built-in functions that accept a variable number of arguments
the list of arguments as an array. You can use primordial function with the
suffix `Apply` (e.g.: `MathMaxApply`, `ArrayPrototypePushApply`) to do that.

## Staging primordials

Conditinally present built-ins can not be primordials. This usually applies
to every experimental feature that either still exists only behind runtime flag,
or is enabled by default but still can be disabled by `--no-` runtime flag.

Instead, these should be stored in `internal/primordials_staging` module.
This module is populated once at pre-execution stage and can not be changed
afterwards.

Whenever a new conditional feature is used within Node.js core, it should be
added to this module instead of getting it directly from globals, to make
it unaffected by userland.

Whenever a conditional feature graduates, it should be added to regular primordials
and removed from staging primordials.

Staging primordials do not automatically adopt every new global object, and do not
replicate nested objects recursively. For example, no internal code requires
`TemporalPlainMonthDay*`, so there's no need to create primordials for it. If
you're adding experimental feature that requires new staging primordial, add it
to the internal module.

### Lazy-loaded staging primordials

Some internal modules are used in early bootstrap, and staging primordials module
might not be initialized yet. If that's the case, do not destructure the module
synchronously, and instead get the built-ins lazily in runtime.

For example, instead of this on top-level of the module:

```js
// For modules that are loaded _after_ pre-exec, this is still safe and preferred
const { Float16Array } = require('internal/primordials_staging');
```

Use either:

```js
// Safe to import synchronously, even though the values are not defined yet
const primordialsStaging = require('internal/primordials_staging');

let SafeFloat16Array;
function numberToFloat16(n) {
SafeFloat16Array ??= primordialsStaging.Float16Array;
return new SafeFloat16Array([ n ])[0];
}
```

Or:

```js
let SafeFloat16Array;
function numberToFloat16(n) {
SafeFloat16Array ??= require('internal/primordials_staging').Float16Array;
return new SafeFloat16Array([ n ])[0];
}
```

## Primordials with known performance issues

One of the reasons why the current Node.js API is not completely tamper-proof is
Expand Down
18 changes: 9 additions & 9 deletions lib/eslint.config_partial.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const noRestrictedSyntax = [
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
},
{
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuotaExceededError)$/])',
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuotaExceededError|WebAssemblyLinkError)$/])',
message: "Use an error exported by 'internal/errors' instead.",
},
{
Expand Down Expand Up @@ -162,11 +162,11 @@ export default [
// disabled with --without-intl build flag.
{
name: 'Intl',
message: 'Use `const { Intl } = globalThis;` instead of the global.',
message: "Use `const { Intl } = require('internal/primordials_staging');` instead of the global.",
},
{
name: 'Iterator',
message: 'Use `const { Iterator } = globalThis;` instead of the global.',
message: "Use `const { Iterator } = require('internal/primordials_staging');` instead of the global.",
},
{
name: 'MessageChannel',
Expand Down Expand Up @@ -248,7 +248,7 @@ export default [
// disabled with --no-harmony-shadow-realm CLI flag.
{
name: 'ShadowRealm',
message: 'Use `const { ShadowRealm } = globalThis;` instead of the global.',
message: "Use `const { ShadowRealm } = require('internal/primordials_staging');` instead of the global.",
},
// SharedArrayBuffer is not available in primordials because it can be
// disabled with --enable-sharedarraybuffer-per-context CLI flag.
Expand All @@ -260,7 +260,7 @@ export default [
// disabled with --no-harmony-temporal CLI flag.
{
name: 'Temporal',
message: 'Use `const { Temporal } = globalThis;` instead of the global.',
message: "Use `const { Temporal } = require('internal/primordials_staging');` instead of the global.",
},
{
name: 'TextDecoder',
Expand Down Expand Up @@ -298,7 +298,7 @@ export default [
// disabled with --jitless CLI flag.
{
name: 'WebAssembly',
message: 'Use `const { WebAssembly } = globalThis;` instead of the global.',
message: "Use `const { WebAssembly } = require('internal/primordials_staging');` instead of the global.",
},
{
name: 'WritableStream',
Expand Down Expand Up @@ -396,17 +396,17 @@ export default [
// disabled with --no-js-float16array CLI flag.
{
name: 'Float16Array',
message: 'Use `const { Float16Array } = globalThis;` instead of the global.',
message: "Use `const { Float16Array } = require('internal/primordials_staging');` instead of the global.",
},
// DisposableStack and AsyncDisposableStack are not available in primordials because they can be
// disabled with --no-js-explicit-resource-management CLI flag.
{
name: 'DisposableStack',
message: 'Use `const { DisposableStack } = globalThis;` instead of the global.',
message: "Use `const { DisposableStack } = require('internal/primordials_staging');` instead of the global.",
},
{
name: 'AsyncDisposableStack',
message: 'Use `const { AsyncDisposableStack } = globalThis;` instead of the global.',
message: "Use `const { AsyncDisposableStack } = require('internal/primordials_staging');` instead of the global.",
},
],
'no-restricted-modules': [
Expand Down
3 changes: 1 addition & 2 deletions lib/internal/freeze_intrinsics.js
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

freeze_intrinsics also runs in pre-execution, so this wouldn't be necessary.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. Still, i'd prefer it to stay for consistency, so it doesn't differ from linter suggestion and the internals never take builtins from globalThis directly.
Unless there is another reason to keep globalThis here.

Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,12 @@ const {
globalThis,
unescape,
} = primordials;

const {
Intl,
SharedArrayBuffer,
Temporal,
WebAssembly,
} = globalThis;
} = require('internal/primordials_staging');

module.exports = function() {
const { Console } = require('internal/console/constructor');
Expand Down
6 changes: 3 additions & 3 deletions lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const {
Symbol,
TypedArrayPrototypeAt,
TypedArrayPrototypeIncludes,
globalThis,
} = primordials;
const primordialsStaging = require('internal/primordials_staging');

const { Buffer } = require('buffer');
const {
Expand Down Expand Up @@ -441,15 +441,15 @@ function nsFromTimeSpecBigInt(sec, nsec) {
let TemporalInstant;

function instantFromNs(nsec) {
TemporalInstant ??= globalThis.Temporal?.Instant;
TemporalInstant ??= primordialsStaging.TemporalInstant;
if (TemporalInstant === undefined) {
throw new ERR_NO_TEMPORAL();
}
return new TemporalInstant(nsec);
}

function instantFromTimeSpecMs(msec, nsec) {
TemporalInstant ??= globalThis.Temporal?.Instant;
TemporalInstant ??= primordialsStaging.TemporalInstant;
if (TemporalInstant === undefined) {
throw new ERR_NO_TEMPORAL();
}
Expand Down
18 changes: 9 additions & 9 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const {
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeStartsWith,
globalThis,
} = primordials;
const primordialsStaging = require('internal/primordials_staging');

const {
compileFunctionForCJSLoader,
Expand Down Expand Up @@ -554,14 +554,14 @@ const wasmInstances = new SafeWeakMap();
translators.set('wasm', function(url, translateContext) {
const { source } = translateContext;
// WebAssembly global is not available during snapshot building, so we need to get it lazily.
const { WebAssembly } = globalThis;
const { WebAssemblyInstance, WebAssemblyLinkError, WebAssemblyModule } = primordialsStaging;
assertBufferSource(source, false, 'load');

debug(`Translating WASMModule ${url}`, translateContext);

let compiled;
try {
compiled = new WebAssembly.Module(source, {
compiled = new WebAssemblyModule(source, {
builtins: ['js-string'],
importedStringConstants: 'wasm:js/string-constants',
});
Expand All @@ -572,28 +572,28 @@ translators.set('wasm', function(url, translateContext) {

const importsList = new SafeSet();
const wasmGlobalImports = [];
for (const impt of WebAssembly.Module.imports(compiled)) {
for (const impt of WebAssemblyModule.imports(compiled)) {
if (impt.kind === 'global') {
ArrayPrototypePush(wasmGlobalImports, impt);
}
// Prefix reservations per https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module.
if (impt.module.startsWith('wasm-js:')) {
throw new WebAssembly.LinkError(`Invalid Wasm import "${impt.module}" in ${url}`);
throw new WebAssemblyLinkError(`Invalid Wasm import "${impt.module}" in ${url}`);
}
if (impt.name.startsWith('wasm:') || impt.name.startsWith('wasm-js:')) {
throw new WebAssembly.LinkError(`Invalid Wasm import name "${impt.module}" in ${url}`);
throw new WebAssemblyLinkError(`Invalid Wasm import name "${impt.module}" in ${url}`);
}
importsList.add(impt.module);
}

const exportsList = new SafeSet();
const wasmGlobalExports = new SafeSet();
for (const expt of WebAssembly.Module.exports(compiled)) {
for (const expt of WebAssemblyModule.exports(compiled)) {
if (expt.kind === 'global') {
wasmGlobalExports.add(expt.name);
}
if (expt.name.startsWith('wasm:') || expt.name.startsWith('wasm-js:')) {
throw new WebAssembly.LinkError(`Invalid Wasm export name "${expt.name}" in ${url}`);
throw new WebAssemblyLinkError(`Invalid Wasm export name "${expt.name}" in ${url}`);
}
exportsList.add(expt.name);
}
Expand All @@ -620,7 +620,7 @@ translators.set('wasm', function(url, translateContext) {
}
// In cycles importing unexecuted Wasm, wasmInstance will be undefined, which will fail during
// instantiation, since all bindings will be in the Temporal Deadzone (TDZ).
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
const { exports } = new WebAssemblyInstance(compiled, reflect.imports);
wasmInstances.set(module.getNamespace(), exports);
for (const expt of exportsList) {
let val = exports[expt];
Expand Down
108 changes: 108 additions & 0 deletions lib/internal/primordials_staging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use strict';

// This file stores JS builtins that can not become primordials yet because
// they can be disabled by runtime flags, or are not enabled by default yet.

// Without this module, the only choice we would have is getting them from
// the global proxy which is mutable from userland. This is especially important
// for lazy-loaded builtins required in modules participating in early bootstrap.
// In such modules, we are usually unable to get these builtins synchronously,
// this applies to synchronous destructuring of this module.
// Importing the module itself is fine at any point, including top level of file.

let _AsyncDisposableStack;
let _DisposableStack;
let _Float16Array;
let _Intl;
let _Iterator;
let _ShadowRealm;
let _SharedArrayBuffer;
let _Temporal;
let _TemporalInstant;
let _Uint8ArrayFromHex;
let _Uint8ArrayFromBase64;
let _WebAssembly;
let _WebAssemblyInstance;
let _WebAssemblyLinkError;
let _WebAssemblyModule;

module.exports = {
get AsyncDisposableStack() {
return _AsyncDisposableStack;
},
get DisposableStack() {
return _DisposableStack;
},
get Float16Array() {
return _Float16Array;
},
get Intl() {
return _Intl;
},
get Iterator() {
return _Iterator;
},
get ShadowRealm() {
return _ShadowRealm;
},
get SharedArrayBuffer() {
return _SharedArrayBuffer;
},
get Temporal() {
return _Temporal;
},
get TemporalInstant() {
return _TemporalInstant;
},
get Uint8ArrayFromHex() {
return _Uint8ArrayFromHex;
},
get Uint8ArrayFromBase64() {
return _Uint8ArrayFromBase64;
},
get WebAssembly() {
return _WebAssembly;
},
get WebAssemblyInstance() {
return _WebAssemblyInstance;
},
get WebAssemblyLinkError() {
return _WebAssemblyLinkError;
},
get WebAssemblyModule() {
return _WebAssemblyModule;
},
_init({
AsyncDisposableStack,
DisposableStack,
Float16Array,
Intl,
Iterator,
ShadowRealm,
SharedArrayBuffer,
Temporal,
Uint8Array: {
fromBase64,
fromHex,
},
WebAssembly,
}) {
Comment on lines +75 to +89
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is only capturing the global object properties, not the object tree, so eg. Temporal.Instant.fromEpochNanoseconds(), new WebAssembly.Module() are still user-mutable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right. There's intentionally no recursive object tree replication because realistically there's no need for that: nested objects that are actually used in internals can be added in follow-ups.
I'll update the docs and add some nested objects (e.g. TemporalInstant) to the current implementation.

_AsyncDisposableStack = AsyncDisposableStack;
_DisposableStack = DisposableStack;
_Float16Array = Float16Array;
_Intl = Intl;
_Iterator = Iterator;
_ShadowRealm = ShadowRealm;
_SharedArrayBuffer = SharedArrayBuffer;
_Temporal = Temporal;
_TemporalInstant = Temporal?.Instant;
_Uint8ArrayFromBase64 = fromBase64;
_Uint8ArrayFromHex = fromHex;
_WebAssembly = WebAssembly;
_WebAssemblyInstance = WebAssembly?.Instance;
_WebAssemblyLinkError = WebAssembly?.LinkError;
_WebAssemblyModule = WebAssembly?.Module;

delete this._init;
},
};
2 changes: 2 additions & 0 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ function prepareExecution(options) {

refreshRuntimeOptions();

require('internal/primordials_staging')._init?.(globalThis);

// Patch the process object and get the resolved main entry point.
const mainEntry = patchProcessObject(expandArgv1);
setupTraceCategoryState();
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ const {
SymbolPrototypeGetDescription,
SymbolReplace,
SymbolSplit,
globalThis,
} = primordials;
const stagingPrimordials = require('internal/primordials_staging');

const {
codes: {
Expand Down Expand Up @@ -246,7 +246,7 @@ function assertCrypto() {
function assertTypeScript() {
if (noTypeScript)
throw new ERR_NO_TYPESCRIPT();
if (globalThis.WebAssembly === undefined)
if (stagingPrimordials.WebAssembly === undefined)
throw new ERR_WEBASSEMBLY_NOT_SUPPORTED('TypeScript');
}

Expand Down
4 changes: 3 additions & 1 deletion lib/internal/util/comparisons.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ const {
Uint8ClampedArray,
WeakMap,
WeakSet,
globalThis: { Float16Array },
} = primordials;
const {
Float16Array,
} = require('internal/primordials_staging');

const { compare } = internalBinding('buffer');
const assert = require('internal/assert');
Expand Down
Loading
Loading