• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Usage of primordials in core
2
3The file `lib/internal/per_context/primordials.js` subclasses and stores the JS
4built-ins that come from the VM so that Node.js built-in modules do not need to
5later look these up from the global proxy, which can be mutated by users.
6
7Usage of primordials should be preferred for any new code, but replacing current
8code with primordials should be
9[done with care](#primordials-with-known-performance-issues). It is highly
10recommended to ping the relevant team when reviewing a pull request that touches
11one of the subsystems they "own".
12
13## Accessing primordials
14
15The primordials are meant for internal use only, and are only accessible for
16internal core modules. User code cannot use or rely on primordials. It is
17usually fine to rely on ECMAScript built-ins and assume that it will behave as
18specified.
19
20If you would like to access the `primordials` object to help you with Node.js
21core development or for tinkering, you can expose it on the global scope using
22this combination of CLI flags:
23
24```bash
25node --expose-internals -r internal/test/binding
26```
27
28## Contents of primordials
29
30### Properties of the global object
31
32Objects and functions on the global object can be deleted or replaced. Using
33them from primordials makes the code more reliable:
34
35```js
36globalThis.Array === primordials.Array; // true
37
38globalThis.Array = function() {
39  return [1, 2, 3];
40};
41globalThis.Array === primordials.Array; // false
42
43primordials.Array(0); // []
44globalThis.Array(0); // [1,2,3]
45```
46
47### Prototype methods
48
49ECMAScript provides a group of methods available on built-in objects that are
50used to interact with JavaScript objects.
51
52```js
53const array = [1, 2, 3];
54array.push(4); // Here `push` refers to %Array.prototype.push%.
55console.log(JSON.stringify(array)); // [1,2,3,4]
56
57// %Array.prototype%.push is modified in userland.
58Array.prototype.push = function push(val) {
59  return this.unshift(val);
60};
61
62array.push(5); // Now `push` refers to the modified method.
63console.log(JSON.stringify(array)); // [5,1,2,3,4]
64```
65
66Primordials wrap the original prototype functions with new functions that take
67the `this` value as the first argument:
68
69```js
70const {
71  ArrayPrototypePush,
72} = primordials;
73
74const array = [1, 2, 3];
75ArrayPrototypePush(array, 4);
76console.log(JSON.stringify(array)); // [1,2,3,4]
77
78Array.prototype.push = function push(val) {
79  return this.unshift(val);
80};
81
82ArrayPrototypePush(array, 5);
83console.log(JSON.stringify(array)); // [1,2,3,4,5]
84```
85
86### Safe classes
87
88Safe classes are classes that provide the same API as their equivalent class,
89but whose implementation aims to avoid any reliance on user-mutable code.
90Safe classes should not be exposed to user-land; use unsafe equivalent when
91dealing with objects that are accessible from user-land.
92
93### Variadic functions
94
95There are some built-in functions that accept a variable number of arguments
96(e.g.: `Math.max`, `%Array.prototype.push%`). It is sometimes useful to provide
97the list of arguments as an array. You can use primordial function with the
98suffix `Apply` (e.g.: `MathMaxApply`, `ArrayPrototypePushApply`) to do that.
99
100## Primordials with known performance issues
101
102One of the reasons why the current Node.js API is not completely tamper-proof is
103performance: sometimes the use of primordials can cause performance regressions
104with V8, which when in a hot code path, could significantly decrease the
105performance of code in Node.js.
106
107* Methods that mutate the internal state of arrays:
108  * `ArrayPrototypePush`
109  * `ArrayPrototypePop`
110  * `ArrayPrototypeShift`
111  * `ArrayPrototypeUnshift`
112* Methods of the function prototype:
113  * `FunctionPrototypeBind`
114  * `FunctionPrototypeCall`: creates performance issues when used to invoke
115    super constructors.
116  * `FunctionPrototype`: use `() => {}` instead when referencing a no-op
117    function.
118* `SafeArrayIterator`
119* `SafeStringIterator`
120* `SafePromiseAll`
121* `SafePromiseAllSettled`
122* `SafePromiseAny`
123* `SafePromiseRace`
124* `SafePromisePrototypeFinally`: use `try {} finally {}` block instead.
125
126In general, when sending or reviewing a PR that makes changes in a hot code
127path, use extra caution and run extensive benchmarks.
128
129## Implicit use of user-mutable methods
130
131### Unsafe array iteration
132
133There are many usual practices in JavaScript that rely on iteration. It's useful
134to be aware of them when dealing with arrays (or `TypedArray`s) in core as array
135iteration typically calls several user-mutable methods. This sections lists the
136most common patterns in which ECMAScript code relies non-explicitly on array
137iteration and how to avoid it.
138
139<details>
140
141<summary>Avoid for-of loops on arrays</summary>
142
143```js
144for (const item of array) {
145  console.log(item);
146}
147```
148
149This code is internally expanded into something that looks like:
150
151```js
152{
153  // 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
154  // 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
155  // 3. Call that function.
156  const iterator = array[Symbol.iterator]();
157  // 1. Lookup `next` property on `iterator` (doesn't exist).
158  // 2. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
159  // 3. Call that function.
160  let { done, value: item } = iterator.next();
161  while (!done) {
162    console.log(item);
163    // Repeat.
164    ({ done, value: item } = iterator.next());
165  }
166}
167```
168
169Instead of utilizing iterators, you can use the more traditional but still very
170performant `for` loop:
171
172```js
173for (let i = 0; i < array.length; i++) {
174  console.log(array[i]);
175}
176```
177
178The following code snippet illustrates how user-land code could impact the
179behavior of internal modules:
180
181```js
182// User-land
183Array.prototype[Symbol.iterator] = () => ({
184  next: () => ({ done: true }),
185});
186
187// Core
188let forOfLoopBlockExecuted = false;
189let forLoopBlockExecuted = false;
190const array = [1, 2, 3];
191for (const item of array) {
192  forOfLoopBlockExecuted = true;
193}
194for (let i = 0; i < array.length; i++) {
195  forLoopBlockExecuted = true;
196}
197console.log(forOfLoopBlockExecuted); // false
198console.log(forLoopBlockExecuted); // true
199```
200
201This only applies if you are working with a genuine array (or array-like
202object). If you are instead expecting an iterator, a for-of loop may be a better
203choice.
204
205</details>
206
207<details>
208
209<summary>Avoid array destructuring assignment on arrays</summary>
210
211```js
212const [first, second] = array;
213```
214
215This is roughly equivalent to:
216
217```js
218// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
219// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
220// 3. Call that function.
221const iterator = array[Symbol.iterator]();
222// 1. Lookup `next` property on `iterator` (doesn't exist).
223// 2. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
224// 3. Call that function.
225const first = iterator.next().value;
226// Repeat.
227const second = iterator.next().value;
228```
229
230Instead you can use object destructuring:
231
232```js
233const { 0: first, 1: second } = array;
234```
235
236or
237
238```js
239const first = array[0];
240const second = array[1];
241```
242
243This only applies if you are working with a genuine array (or array-like
244object). If you are instead expecting an iterator, array destructuring is the
245best choice.
246
247</details>
248
249<details>
250
251<summary>Avoid spread operator on arrays</summary>
252
253```js
254// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
255// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
256// 3. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
257const arrayCopy = [...array];
258func(...array);
259```
260
261Instead you can use other ECMAScript features to achieve the same result:
262
263```js
264const arrayCopy = ArrayPrototypeSlice(array);
265ReflectApply(func, null, array);
266```
267
268</details>
269
270<details>
271
272<summary><code>%Array.prototype.concat%</code> looks up
273         <code>@@isConcatSpreadable</code> property of the passed
274         arguments and the <code>this</code> value.</summary>
275
276```js
277{
278  // Unsafe code example:
279  // 1. Lookup @@isConcatSpreadable property on `array` (user-mutable if
280  //    user-provided).
281  // 2. Lookup @@isConcatSpreadable property on `%Array.prototype%
282  //    (user-mutable).
283  // 2. Lookup @@isConcatSpreadable property on `%Object.prototype%
284  //    (user-mutable).
285  const array = [];
286  ArrayPrototypeConcat(array);
287}
288```
289
290```js
291// User-land
292Object.defineProperty(Object.prototype, Symbol.isConcatSpreadable, {
293  get() {
294    this.push(5);
295    return true;
296  },
297});
298
299// Core
300{
301  // Using ArrayPrototypeConcat does not produce the expected result:
302  const a = [1, 2];
303  const b = [3, 4];
304  console.log(ArrayPrototypeConcat(a, b)); // [1, 2, 5, 3, 4, 5]
305}
306{
307  // Concatenating two arrays can be achieved safely, e.g.:
308  const a = [1, 2];
309  const b = [3, 4];
310  // Using %Array.prototype.push% and `SafeArrayIterator` to get the expected
311  // outcome:
312  const concatArray = [];
313  ArrayPrototypePush(concatArray, ...new SafeArrayIterator(a),
314                     ...new SafeArrayIterator(b));
315  console.log(concatArray); // [1, 2, 3, 4]
316
317  // Or using `ArrayPrototypePushApply` if it's OK to mutate the first array:
318  ArrayPrototypePushApply(a, b);
319  console.log(a); // [1, 2, 3, 4]
320}
321```
322
323</details>
324
325<details>
326
327<summary><code>%Object.fromEntries%</code> iterate over an array</summary>
328
329```js
330{
331  // Unsafe code example:
332  // 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
333  // 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
334  // 3. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
335  const obj = ObjectFromEntries(array);
336}
337
338{
339  // Safe example using `SafeArrayIterator`:
340  const obj = ObjectFromEntries(new SafeArrayIterator(array));
341}
342
343{
344  // Safe example without using `SafeArrayIterator`:
345  const obj = {};
346  for (let i = 0; i < array.length; i++) {
347    obj[array[i][0]] = array[i][1];
348  }
349  // In a hot code path, this would be the preferred method.
350}
351```
352
353</details>
354
355<details>
356
357<summary><code>%Promise.all%</code>,
358         <code>%Promise.allSettled%</code>,
359         <code>%Promise.any%</code>, and
360         <code>%Promise.race%</code> iterate over an array</summary>
361
362```js
363// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
364// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
365// 3. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
366// 4. Lookup `then` property on %Array.Prototype% (user-mutable).
367// 5. Lookup `then` property on %Object.Prototype% (user-mutable).
368PromiseAll([]); // unsafe
369
370// 1. Lookup `then` property on %Array.Prototype% (user-mutable).
371// 2. Lookup `then` property on %Object.Prototype% (user-mutable).
372PromiseAll(new SafeArrayIterator([])); // still unsafe
373SafePromiseAll([]); // still unsafe
374
375SafePromiseAllReturnVoid([]); // safe
376SafePromiseAllReturnArrayLike([]); // safe
377
378const array = [promise];
379const set = new SafeSet().add(promise);
380// When running one of these functions on a non-empty iterable, it will also:
381// 1. Lookup `then` property on `promise` (user-mutable if user-provided).
382// 2. Lookup `then` property on `%Promise.prototype%` (user-mutable).
383// 3. Lookup `then` property on %Array.Prototype% (user-mutable).
384// 4. Lookup `then` property on %Object.Prototype% (user-mutable).
385PromiseAll(new SafeArrayIterator(array)); // unsafe
386PromiseAll(set); // unsafe
387
388SafePromiseAllReturnVoid(array); // safe
389SafePromiseAllReturnArrayLike(array); // safe
390
391// Some key differences between `SafePromise[...]` and `Promise[...]` methods:
392
393// 1. SafePromiseAll, SafePromiseAllSettled, SafePromiseAny, SafePromiseRace,
394//    SafePromiseAllReturnArrayLike, SafePromiseAllReturnVoid, and
395//    SafePromiseAllSettledReturnVoid support passing a mapperFunction as second
396//    argument.
397SafePromiseAll(ArrayPrototypeMap(array, someFunction));
398SafePromiseAll(array, someFunction); // Same as the above, but more efficient.
399
400// 2. SafePromiseAll, SafePromiseAllSettled, SafePromiseAny, SafePromiseRace,
401//    SafePromiseAllReturnArrayLike, SafePromiseAllReturnVoid, and
402//    SafePromiseAllSettledReturnVoid only support arrays and array-like
403//    objects, not iterables. Use ArrayFrom to convert an iterable to an array.
404SafePromiseAllReturnVoid(set); // ignores set content.
405SafePromiseAllReturnVoid(ArrayFrom(set)); // works
406
407// 3. SafePromiseAllReturnArrayLike is safer than SafePromiseAll, however you
408//    should not use them when its return value is passed to the user as it can
409//    be surprising for them not to receive a genuine array.
410SafePromiseAllReturnArrayLike(array).then((val) => val instanceof Array); // false
411SafePromiseAll(array).then((val) => val instanceof Array); // true
412```
413
414</details>
415
416<details>
417
418<summary><code>%Map%</code>, <code>%Set%</code>, <code>%WeakMap%</code>, and
419         <code>%WeakSet%</code> constructors iterate over an array</summary>
420
421```js
422// User-land
423Array.prototype[Symbol.iterator] = () => ({
424  next: () => ({ done: true }),
425});
426
427// Core
428
429// 1. Lookup @@iterator property on %Array.prototype% (user-mutable).
430// 2. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
431const set = new SafeSet([1, 2, 3]);
432
433console.log(set.size); // 0
434```
435
436```js
437// User-land
438Array.prototype[Symbol.iterator] = () => ({
439  next: () => ({ done: true }),
440});
441
442// Core
443const set = new SafeSet();
444set.add(1).add(2).add(3);
445console.log(set.size); // 3
446```
447
448</details>
449
450### Promise objects
451
452<details>
453
454<summary><code>%Promise.prototype.finally%</code> looks up <code>then</code>
455         property of the Promise instance</summary>
456
457```js
458// User-land
459Promise.prototype.then = function then(a, b) {
460  return Promise.resolve();
461};
462
463// Core
464let finallyBlockExecuted = false;
465PromisePrototypeFinally(somePromiseThatEventuallySettles,
466                        () => { finallyBlockExecuted = true; });
467process.on('exit', () => console.log(finallyBlockExecuted)); // false
468```
469
470```js
471// User-land
472Promise.prototype.then = function then(a, b) {
473  return Promise.resolve();
474};
475
476// Core
477let finallyBlockExecuted = false;
478(async () => {
479  try {
480    return await somePromiseThatEventuallySettles;
481  } finally {
482    finallyBlockExecuted = true;
483  }
484})();
485process.on('exit', () => console.log(finallyBlockExecuted)); // true
486```
487
488</details>
489
490<details>
491
492<summary><code>%Promise.all%</code>,
493         <code>%Promise.allSettled%</code>,
494         <code>%Promise.any%</code>, and
495         <code>%Promise.race%</code> look up <code>then</code>
496         property of the Promise instances</summary>
497
498You can use safe alternatives from primordials that differ slightly from the
499original methods:
500
501* It expects an array (or array-like object) instead of an iterable.
502* It wraps each promise in `SafePromise` objects and wraps the result in a new
503  `Promise` instance – which may come with a performance penalty.
504* It accepts a `mapperFunction` as second argument.
505* Because it doesn't look up `then` property, it may not be the right tool to
506  handle user-provided promises (which may be instances of a subclass of
507  `Promise`).
508
509```js
510// User-land
511Promise.prototype.then = function then(a, b) {
512  return Promise.resolve();
513};
514
515// Core
516let thenBlockExecuted = false;
517PromisePrototypeThen(
518  PromiseAll(new SafeArrayIterator([PromiseResolve()])),
519  () => { thenBlockExecuted = true; },
520);
521process.on('exit', () => console.log(thenBlockExecuted)); // false
522```
523
524```js
525// User-land
526Promise.prototype.then = function then(a, b) {
527  return Promise.resolve();
528};
529
530// Core
531let thenBlockExecuted = false;
532PromisePrototypeThen(
533  SafePromiseAll([PromiseResolve()]),
534  () => { thenBlockExecuted = true; },
535);
536process.on('exit', () => console.log(thenBlockExecuted)); // true
537```
538
539A common pattern is to map on the array of `Promise`s to apply some
540transformations, in that case it can be more efficient to pass a second argument
541rather than invoking `%Array.prototype.map%`.
542
543```js
544SafePromiseAll(ArrayPrototypeMap(array, someFunction));
545SafePromiseAll(array, someFunction); // Same as the above, but more efficient.
546```
547
548</details>
549
550### (Async) Generator functions
551
552Generators and async generators returned by generator functions and async
553generator functions are relying on user-mutable methods; their use in core
554should be avoided.
555
556<details>
557
558<summary><code>%GeneratorFunction.prototype.prototype%.next</code> is
559         user-mutable</summary>
560
561```js
562// User-land
563Object.getPrototypeOf(function* () {}).prototype.next = function next() {
564  return { done: true };
565};
566
567// Core
568function* someGenerator() {
569  yield 1;
570  yield 2;
571  yield 3;
572}
573let loopCodeExecuted = false;
574for (const nb of someGenerator()) {
575  loopCodeExecuted = true;
576}
577console.log(loopCodeExecuted); // false
578```
579
580</details>
581
582<details>
583
584<summary><code>%AsyncGeneratorFunction.prototype.prototype%.next</code> is
585         user-mutable</summary>
586
587```js
588// User-land
589Object.getPrototypeOf(async function* () {}).prototype.next = function next() {
590  return new Promise(() => {});
591};
592
593// Core
594async function* someGenerator() {
595  yield 1;
596  yield 2;
597  yield 3;
598}
599let finallyBlockExecuted = false;
600async () => {
601  try {
602    for await (const nb of someGenerator()) {
603      // some code;
604    }
605  } finally {
606    finallyBlockExecuted = true;
607  }
608};
609process.on('exit', () => console.log(finallyBlockExecuted)); // false
610```
611
612</details>
613
614### Text processing
615
616#### Unsafe string methods
617
618| The string method             | looks up the property |
619| ----------------------------- | --------------------- |
620| `String.prototype.match`      | `Symbol.match`        |
621| `String.prototype.matchAll`   | `Symbol.matchAll`     |
622| `String.prototype.replace`    | `Symbol.replace`      |
623| `String.prototype.replaceAll` | `Symbol.replace`      |
624| `String.prototype.search`     | `Symbol.search`       |
625| `String.prototype.split`      | `Symbol.split`        |
626
627```js
628// User-land
629RegExp.prototype[Symbol.replace] = () => 'foo';
630String.prototype[Symbol.replace] = () => 'baz';
631
632// Core
633console.log(StringPrototypeReplace('ber', /e/, 'a')); // 'foo'
634console.log(StringPrototypeReplace('ber', 'e', 'a')); // 'baz'
635console.log(RegExpPrototypeSymbolReplace(/e/, 'ber', 'a')); // 'bar'
636```
637
638#### Unsafe string iteration
639
640As with arrays, iterating over strings calls several user-mutable methods. Avoid
641iterating over strings when possible, or use `SafeStringIterator`.
642
643#### Unsafe `RegExp` methods
644
645Functions that lookup the `exec` property on the prototype chain:
646
647* `RegExp.prototype[Symbol.match]`
648* `RegExp.prototype[Symbol.matchAll]`
649* `RegExp.prototype[Symbol.replace]`
650* `RegExp.prototype[Symbol.search]`
651* `RegExp.prototype[Symbol.split]`
652* `RegExp.prototype.test`
653
654```js
655// User-land
656RegExp.prototype.exec = () => null;
657
658// Core
659console.log(RegExpPrototypeTest(/o/, 'foo')); // false
660console.log(RegExpPrototypeExec(/o/, 'foo') !== null); // true
661
662console.log(RegExpPrototypeSymbolSearch(/o/, 'foo')); // -1
663console.log(SafeStringPrototypeSearch('foo', /o/)); // 1
664```
665
666#### Don't trust `RegExp` flags
667
668RegExp flags are not own properties of the regex instances, which means flags
669can be reset from user-land.
670
671<details>
672
673<summary>List of <code>RegExp</code> methods that look up properties from
674         mutable getters</summary>
675
676| `RegExp` method                | looks up the following flag-related properties                     |
677| ------------------------------ | ------------------------------------------------------------------ |
678| `get RegExp.prototype.flags`   | `global`, `ignoreCase`, `multiline`, `dotAll`, `unicode`, `sticky` |
679| `RegExp.prototype[@@match]`    | `global`, `unicode`                                                |
680| `RegExp.prototype[@@matchAll]` | `flags`                                                            |
681| `RegExp.prototype[@@replace]`  | `global`, `unicode`                                                |
682| `RegExp.prototype[@@split]`    | `flags`                                                            |
683| `RegExp.prototype.toString`    | `flags`                                                            |
684
685</details>
686
687```js
688// User-land
689Object.defineProperty(RegExp.prototype, 'global', { value: false });
690
691// Core
692console.log(RegExpPrototypeSymbolReplace(/o/g, 'foo', 'a')); // 'fao'
693console.log(RegExpPrototypeSymbolReplace(hardenRegExp(/o/g), 'foo', 'a')); // 'faa'
694```
695
696### Defining object own properties
697
698When defining property descriptor (to add or update an own property to a
699JavaScript object), be sure to always use a null-prototype object to avoid
700prototype pollution.
701
702```js
703// User-land
704Object.prototype.get = function get() {};
705
706// Core
707try {
708  ObjectDefineProperty({}, 'someProperty', { value: 0 });
709} catch (err) {
710  console.log(err); // TypeError: Invalid property descriptor.
711}
712```
713
714```js
715// User-land
716Object.prototype.get = function get() {};
717
718// Core
719ObjectDefineProperty({}, 'someProperty', { __proto__: null, value: 0 });
720console.log('no errors'); // no errors.
721```
722
723Same applies when trying to modify an existing property, e.g. trying to make a
724read-only property enumerable:
725
726```js
727// User-land
728Object.prototype.value = 'Unrelated user-provided data';
729
730// Core
731class SomeClass {
732  get readOnlyProperty() { return 'genuine data'; }
733}
734ObjectDefineProperty(SomeClass.prototype, 'readOnlyProperty', { enumerable: true });
735console.log(new SomeClass().readOnlyProperty); // Unrelated user-provided data
736```
737
738```js
739// User-land
740Object.prototype.value = 'Unrelated user-provided data';
741
742// Core
743const kEnumerableProperty = { __proto__: null, enumerable: true };
744// In core, use const {kEnumerableProperty} = require('internal/util');
745class SomeClass {
746  get readOnlyProperty() { return 'genuine data'; }
747}
748ObjectDefineProperty(SomeClass.prototype, 'readOnlyProperty', kEnumerableProperty);
749console.log(new SomeClass().readOnlyProperty); // genuine data
750```
751
752### Defining a `Proxy` handler
753
754When defining a `Proxy`, the handler object could be at risk of prototype
755pollution when using a plain object literal:
756
757```js
758// User-land
759Object.prototype.get = () => 'Unrelated user-provided data';
760
761// Core
762const objectToProxy = { someProperty: 'genuine value' };
763
764const proxyWithPlainObjectLiteral = new Proxy(objectToProxy, {
765  has() { return false; },
766});
767console.log(proxyWithPlainObjectLiteral.someProperty); // Unrelated user-provided data
768
769const proxyWithNullPrototypeObject = new Proxy(objectToProxy, {
770  __proto__: null,
771  has() { return false; },
772});
773console.log(proxyWithNullPrototypeObject.someProperty); // genuine value
774```
775