diff --git a/doc/contributing/primordials.md b/doc/contributing/primordials.md
new file mode 100644
index 00000000000000..fe20fdd9094d04
--- /dev/null
+++ b/doc/contributing/primordials.md
@@ -0,0 +1,595 @@
+# Usage of primordials in core
+
+The file `lib/internal/per_context/primordials.js` subclasses and stores the JS
+built-ins that come from the VM so that Node.js built-in modules do not need to
+later look these up from the global proxy, which can be mutated by users.
+
+Usage of primordials should be preferred for any new code, but replacing current
+code with primordials should be
+[done with care](#primordials-with-known-performance-issues). It is highly
+recommended to ping the relevant team when reviewing a pull request that touches
+one of the subsystems they "own".
+
+## Accessing primordials
+
+The primordials are meant for internal use only, and are only accessible for
+internal core modules. User code cannot use or rely on primordials. It is
+usually fine to rely on ECMAScript built-ins and assume that it will behave as
+specified.
+
+If you would like to access the `primordials` object to help you with Node.js
+core development or for tinkering, you can expose it on the global scope using
+this combination of CLI flags:
+
+```bash
+node --expose-internals -r internal/test/binding
+```
+
+## Contents of primordials
+
+### Properties of the global object
+
+Objects and functions on the global object can be deleted or replaced. Using
+them from primordials makes the code more reliable:
+
+```js
+globalThis.Array === primordials.Array; // true
+
+globalThis.Array = function() {
+ return [1, 2, 3];
+};
+globalThis.Array === primordials.Array; // false
+
+primordials.Array(0); // []
+globalThis.Array(0); // [1,2,3]
+```
+
+### Prototype methods
+
+ECMAScript provides a group of methods available on built-in objects that are
+used to interact with JavaScript objects.
+
+```js
+const array = [1, 2, 3];
+array.push(4); // Here `push` refers to %Array.prototype.push%.
+console.log(JSON.stringify(array)); // [1,2,3,4]
+
+// %Array.prototype%.push is modified in userland.
+Array.prototype.push = function push(val) {
+ return this.unshift(val);
+};
+
+array.push(5); // Now `push` refers to the modified method.
+console.log(JSON.stringify(array)); // [5,1,2,3,4]
+```
+
+Primordials wrap the original prototype functions with new functions that take
+the `this` value as the first argument:
+
+```js
+const {
+ ArrayPrototypePush,
+} = primordials;
+
+const array = [1, 2, 3];
+ArrayPrototypePush(array, 4);
+console.log(JSON.stringify(array)); // [1,2,3,4]
+
+Array.prototype.push = function push(val) {
+ return this.unshift(val);
+};
+
+ArrayPrototypePush(array, 5);
+console.log(JSON.stringify(array)); // [1,2,3,4,5]
+```
+
+### Safe classes
+
+Safe classes are classes that provide the same API as their equivalent class,
+but whose implementation aims to avoid any reliance on user-mutable code.
+Safe classes should not be exposed to user-land; use unsafe equivalent when
+dealing with objects that are accessible from user-land.
+
+### Variadic functions
+
+There are some built-in functions that accept a variable number of arguments
+(e.g.: `Math.max`, `%Array.prototype.push%`). It is sometimes useful to provide
+the list of arguments as an array. You can use primordial function with the
+suffix `Apply` (e.g.: `MathMaxApply`, `ArrayPrototypePushApply`) to do that.
+
+## Primordials with known performance issues
+
+One of the reasons why the current Node.js API is not completely tamper-proof is
+performance: sometimes the use of primordials can cause performance regressions
+with V8, which when in a hot code path, could significantly decrease the
+performance of code in Node.js.
+
+* Methods that mutate the internal state of arrays:
+ * `ArrayPrototypePush`
+ * `ArrayPrototypePop`
+ * `ArrayPrototypeShift`
+ * `ArrayPrototypeUnshift`
+* Methods of the function prototype:
+ * `FunctionPrototypeBind`
+ * `FunctionPrototypeCall`: creates performance issues when used to invoke
+ super constructors.
+ * `FunctionPrototype`: use `() => {}` instead when referencing a no-op
+ function.
+* `SafeArrayIterator`
+* `SafeStringIterator`
+* `SafePromiseAll`
+* `SafePromiseAllSettled`
+* `SafePromiseAny`
+* `SafePromiseRace`
+* `SafePromisePrototypeFinally`: use `try {} finally {}` block instead.
+
+In general, when sending or reviewing a PR that makes changes in a hot code
+path, use extra caution and run extensive benchmarks.
+
+## Implicit use of user-mutable methods
+
+### Unsafe array iteration
+
+There are many usual practices in JavaScript that rely on iteration. It's useful
+to be aware of them when dealing with arrays (or `TypedArray`s) in core as array
+iteration typically calls several user-mutable methods. This sections lists the
+most common patterns in which ECMAScript code relies non-explicitly on array
+iteration and how to avoid it.
+
+
+
+Avoid for-of loops on arrays
+
+```js
+for (const item of array) {
+ console.log(item);
+}
+```
+
+This code is internally expanded into something that looks like:
+
+```js
+{
+ // 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
+ // 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
+ // 3. Call that function.
+ const iterator = array[Symbol.iterator]();
+ // 1. Lookup `next` property on `iterator` (doesn't exist).
+ // 2. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
+ // 3. Call that function.
+ let { done, value: item } = iterator.next();
+ while (!done) {
+ console.log(item);
+ // Repeat.
+ ({ done, value: item } = iterator.next());
+ }
+}
+```
+
+Instead of utilizing iterators, you can use the more traditional but still very
+performant `for` loop:
+
+```js
+for (let i = 0; i < array.length; i++) {
+ console.log(array[i]);
+}
+```
+
+The following code snippet illustrates how user-land code could impact the
+behavior of internal modules:
+
+```js
+// User-land
+Array.prototype[Symbol.iterator] = () => ({
+ next: () => ({ done: true }),
+});
+
+// Core
+let forOfLoopBlockExecuted = false;
+let forLoopBlockExecuted = false;
+const array = [1, 2, 3];
+for (const item of array) {
+ forOfLoopBlockExecuted = true;
+}
+for (let i = 0; i < array.length; i++) {
+ forLoopBlockExecuted = true;
+}
+console.log(forOfLoopBlockExecuted); // false
+console.log(forLoopBlockExecuted); // true
+```
+
+This only applies if you are working with a genuine array (or array-like
+object). If you are instead expecting an iterator, a for-of loop may be a better
+choice.
+
+
+
+
+
+Avoid array destructuring assignment on arrays
+
+```js
+const [first, second] = array;
+```
+
+This is roughly equivalent to:
+
+```js
+// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
+// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
+// 3. Call that function.
+const iterator = array[Symbol.iterator]();
+// 1. Lookup `next` property on `iterator` (doesn't exist).
+// 2. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
+// 3. Call that function.
+const first = iterator.next().value;
+// Repeat.
+const second = iterator.next().value;
+```
+
+Instead you can use object destructuring:
+
+```js
+const { 0: first, 1: second } = array;
+```
+
+or
+
+```js
+const first = array[0];
+const second = array[1];
+```
+
+This only applies if you are working with a genuine array (or array-like
+object). If you are instead expecting an iterator, array destructuring is the
+best choice.
+
+
+
+
+
+Avoid spread operator on arrays
+
+```js
+// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
+// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
+// 3. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
+const arrayCopy = [...array];
+func(...array);
+```
+
+Instead you can use other ECMAScript features to achieve the same result:
+
+```js
+const arrayCopy = ArrayPrototypeSlice(array);
+ReflectApply(func, null, array);
+```
+
+
+
+
+
+%Object.fromEntries%
iterate over an array
+
+```js
+{
+ // Unsafe code example:
+ // 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
+ // 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
+ // 3. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
+ const obj = ObjectFromEntries(array);
+}
+
+{
+ // Safe example using `SafeArrayIterator`:
+ const obj = ObjectFromEntries(new SafeArrayIterator(array));
+}
+
+{
+ // Safe example without using `SafeArrayIterator`:
+ const obj = {};
+ for (let i = 0; i < array.length; i++) {
+ obj[array[i][0]] = array[i][1];
+ }
+ // In a hot code path, this would be the preferred method.
+}
+```
+
+
+
+
+
+%Promise.all%
,
+ %Promise.allSettled%
,
+ %Promise.any%
, and
+ %Promise.race%
iterate over an array
+
+```js
+// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
+// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
+// 3. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
+PromiseAll(array); // unsafe
+
+PromiseAll(new SafeArrayIterator(array));
+SafePromiseAll(array); // safe
+```
+
+
+
+
+
+%Map%
, %Set%
, %WeakMap%
, and
+ %WeakSet%
constructors iterate over an array
+
+```js
+// User-land
+Array.prototype[Symbol.iterator] = () => ({
+ next: () => ({ done: true }),
+});
+
+// Core
+
+// 1. Lookup @@iterator property on %Array.prototype% (user-mutable).
+// 2. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
+const set = new SafeSet([1, 2, 3]);
+
+console.log(set.size); // 0
+```
+
+```js
+// User-land
+Array.prototype[Symbol.iterator] = () => ({
+ next: () => ({ done: true }),
+});
+
+// Core
+const set = new SafeSet();
+set.add(1).add(2).add(3);
+console.log(set.size); // 3
+```
+
+
+
+### Promise objects
+
+
+
+%Promise.prototype.finally%
looks up then
+ property of the Promise instance
+
+```js
+// User-land
+Promise.prototype.then = function then(a, b) {
+ return Promise.resolve();
+};
+
+// Core
+let finallyBlockExecuted = false;
+PromisePrototypeFinally(somePromiseThatEventuallySettles,
+ () => { finallyBlockExecuted = true; });
+process.on('exit', () => console.log(finallyBlockExecuted)); // false
+```
+
+```js
+// User-land
+Promise.prototype.then = function then(a, b) {
+ return Promise.resolve();
+};
+
+// Core
+let finallyBlockExecuted = false;
+(async () => {
+ try {
+ return await somePromiseThatEventuallySettles;
+ } finally {
+ finallyBlockExecuted = true;
+ }
+})();
+process.on('exit', () => console.log(finallyBlockExecuted)); // true
+```
+
+
+
+
+
+%Promise.all%
,
+ %Promise.allSettled%
,
+ %Promise.any%
, and
+ %Promise.race%
look up then
+ property of the Promise instances
+
+You can use safe alternatives from primordials that differ slightly from the
+original methods:
+* It expects an array (or array-like object) instead of an iterable.
+* It wraps each promise in `SafePromise` objects and wraps the result in a new
+ `Promise` instance – which may come with a performance penalty.
+* Because it doesn't look up `then` property, it may not be the right tool to
+ handle user-provided promises (which may be instances of a subclass of
+ `Promise`).
+
+```js
+// User-land
+Promise.prototype.then = function then(a, b) {
+ return Promise.resolve();
+};
+
+// Core
+let thenBlockExecuted = false;
+PromisePrototypeThen(
+ PromiseAll(new SafeArrayIterator([PromiseResolve()])),
+ () => { thenBlockExecuted = true; }
+);
+process.on('exit', () => console.log(thenBlockExecuted)); // false
+```
+
+```js
+// User-land
+Promise.prototype.then = function then(a, b) {
+ return Promise.resolve();
+};
+
+// Core
+let thenBlockExecuted = false;
+PromisePrototypeThen(
+ SafePromiseAll([PromiseResolve()]),
+ () => { thenBlockExecuted = true; }
+);
+process.on('exit', () => console.log(thenBlockExecuted)); // true
+```
+
+
+
+### (Async) Generator functions
+
+Generators and async generators returned by generator functions and async
+generator functions are relying on user-mutable methods; their use in core
+should be avoided.
+
+
+
+%GeneratorFunction.prototype.prototype%.next
is
+ user-mutable
+
+```js
+// User-land
+Object.getPrototypeOf(function* () {}).prototype.next = function next() {
+ return { done: true };
+};
+
+// Core
+function* someGenerator() {
+ yield 1;
+ yield 2;
+ yield 3;
+}
+let loopCodeExecuted = false;
+for (const nb of someGenerator()) {
+ loopCodeExecuted = true;
+}
+console.log(loopCodeExecuted); // false
+```
+
+
+
+
+
+%AsyncGeneratorFunction.prototype.prototype%.next
is
+ user-mutable
+
+```js
+// User-land
+Object.getPrototypeOf(async function* () {}).prototype.next = function next() {
+ return new Promise(() => {});
+};
+
+// Core
+async function* someGenerator() {
+ yield 1;
+ yield 2;
+ yield 3;
+}
+let finallyBlockExecuted = false;
+async () => {
+ try {
+ for await (const nb of someGenerator()) {
+ // some code;
+ }
+ } finally {
+ finallyBlockExecuted = true;
+ }
+};
+process.on('exit', () => console.log(finallyBlockExecuted)); // false
+```
+
+
+
+### Text processing
+
+#### Unsafe string methods
+
+| The string method | looks up the property |
+| ----------------------------- | --------------------- |
+| `String.prototype.match` | `Symbol.match` |
+| `String.prototype.matchAll` | `Symbol.matchAll` |
+| `String.prototype.replace` | `Symbol.replace` |
+| `String.prototype.replaceAll` | `Symbol.replace` |
+| `String.prototype.search` | `Symbol.search` |
+| `String.prototype.split` | `Symbol.split` |
+
+```js
+// User-land
+RegExp.prototype[Symbol.replace] = () => 'foo';
+String.prototype[Symbol.replace] = () => 'baz';
+
+// Core
+console.log(StringPrototypeReplace('ber', /e/, 'a')); // 'foo'
+console.log(StringPrototypeReplace('ber', 'e', 'a')); // 'baz'
+console.log(RegExpPrototypeSymbolReplace(/e/, 'ber', 'a')); // 'bar'
+```
+
+#### Unsafe string iteration
+
+As with arrays, iterating over strings calls several user-mutable methods. Avoid
+iterating over strings when possible, or use `SafeStringIterator`.
+
+#### Unsafe `RegExp` methods
+
+Functions that lookup the `exec` property on the prototype chain:
+
+* `RegExp.prototype[Symbol.match]`
+* `RegExp.prototype[Symbol.matchAll]`
+* `RegExp.prototype[Symbol.replace]`
+* `RegExp.prototype[Symbol.search]`
+* `RegExp.prototype[Symbol.split]`
+* `RegExp.prototype.test`
+
+```js
+// User-land
+RegExp.prototype.exec = () => null;
+
+// Core
+console.log(RegExpPrototypeTest(/o/, 'foo')); // false
+console.log(RegExpPrototypeExec(/o/, 'foo') !== null); // true
+```
+
+#### Don't trust `RegExp` flags
+
+RegExp flags are not own properties of the regex instances, which means flags
+can be reset from user-land.
+
+
+
+List of RegExp
methods that look up properties from
+ mutable getters
+
+| `RegExp` method | looks up the following flag-related properties |
+| ------------------------------ | ------------------------------------------------------------------ |
+| `get RegExp.prototype.flags` | `global`, `ignoreCase`, `multiline`, `dotAll`, `unicode`, `sticky` |
+| `RegExp.prototype[@@match]` | `global`, `unicode` |
+| `RegExp.prototype[@@matchAll]` | `flags` |
+| `RegExp.prototype[@@replace]` | `global`, `unicode` |
+| `RegExp.prototype[@@split]` | `flags` |
+| `RegExp.prototype.toString` | `flags` |
+
+
+
+```js
+// User-land
+Object.defineProperty(RegExp.prototype, 'global', { value: false });
+
+// Core
+console.log(RegExpPrototypeSymbolReplace(/o/g, 'foo', 'a')); // 'fao'
+
+const regex = /o/g;
+ObjectDefineProperties(regex, {
+ dotAll: { value: false },
+ exec: { value: undefined },
+ flags: { value: 'g' },
+ global: { value: true },
+ ignoreCase: { value: false },
+ multiline: { value: false },
+ unicode: { value: false },
+ sticky: { value: false },
+});
+console.log(RegExpPrototypeSymbolReplace(regex, 'foo', 'a')); // 'faa'
+```