This repository is an independent benchmark lab for scoped robustness in JavaScript runtime internals.
Main claim:
This is not a security sandbox. This is a scoped robustness model for Node internals.
This PoC does not replace primordials as a primitive. It challenges pervasive hand-written primordials as a policy.
The alternative model is:
- explicit robustness contracts;
- explicit internal operations;
- specialized containers for hot paths;
- static policy checks;
- benchmark gates.
This benchmark compares cost models, not complete production designs. A faster result in one workload does not imply universal superiority.
The project compares three runtime variants:
01-baseline: ordinary dynamic JavaScript using mutable built-ins.02-primordials: captured primordial-style operations.03-explicit-contract(scoped internal ops): explicit internal operations plus specialized internal containers for hot paths.
The project provides evidence for four separate claims, rather than one vague "better than primordials" claim:
| Claim | Evidence |
|---|---|
| Correctness | same observable result before pollution |
| Robustness | selected internals keep working after post-bootstrap monkey-patching |
| Performance characteristics | benchmark with warmup, samples, median, p95, p99, mean, stddev, baseline slowdown |
| Security boundary non-claim | explicit threat model says this is not a sandbox |
Security claim: none.
Robustness claim: selected internal paths keep working after post-bootstrap prototype pollution.
This PoC claims:
- selected internal paths can remain robust after post-bootstrap prototype pollution;
- explicit internal operations can avoid whole-program type inference;
- specialized internal containers can reduce overhead in hot paths;
- robustness policy can be tested and statically checked.
This PoC does not claim:
- to replace all primordials in Node.js;
- to provide a security sandbox;
- to make arbitrary JavaScript safe;
- to infer receiver types for dynamic method calls;
- to shift literal prototypes without a separate realm;
- to provide Node-wide performance conclusions.
Correctness and robustness gates:
npm run checkMain benchmark:
npm run benchSmoke benchmark for CI runners:
npm run bench:smokeSoft performance guard after a benchmark run:
npm run bench:guardEverything for local validation:
npm run allThe benchmark command accepts positional arguments:
node tools/bench.js <iterations> <samples> <warmup>Preset benchmark scripts:
npm run bench:smoke:200iterations,50samples,20warmup.npm run bench:ci:1000iterations,300samples,100warmup.npm run bench:perf:5000iterations,2000samples,300warmup.
Defaults:
- iterations:
1000 - samples:
1000 - warmup:
100
Reports:
reports/verification.mdreports/latest.md
Generated by npm run check:
| Test | baseline | primordials | explicit-contract |
|---|---|---|---|
| normal correctness | pass | pass | pass |
| after Array.prototype.push pollution | fail | pass | pass |
| after Array.prototype.join pollution | fail | pass | pass |
| internal queue FIFO after pollution | fail | pass | pass |
| after Promise.resolve pollution | fail | pass | pass |
| mixed workload after pollution | fail | pass | pass |
| instanceof returned values | pass | pass | pass |
| no globalThis mutation | pass | pass | pass |
| literal prototypes shifted | fail | fail | fail |
| pre-bootstrap pollution protected | outside-model | outside-model | outside-model |
| exposed internal object protected | outside-model | outside-model | outside-model |
The literal prototypes shifted row is intentional. This model does not shift
literal prototypes. Array/object literals still belong to the current realm
unless a separate realm is used.
The outside-model rows are also intentional. Pre-bootstrap pollution is
outside the trusted bootstrap boundary, and deliberately exposed internal
objects remain mutable JavaScript objects.
Each runtime exposes the same API:
runtime.sumNumbers([1, 2, 3]);
runtime.formatDiagnostic(["sum", "6", "done"]);
const queue = runtime.internalQueue();
queue.push("a");
queue.shift();
queue.isEmpty();
runtime.resolveValue("value");The baseline uses ordinary dynamic calls such as .reduce(), .push(),
.shift(), .join(), and Promise.resolve().
The primordials variant captures operations at runtime creation time and uses those captured operations after pollution.
The explicit-contract variant, also described as scoped internal ops, uses
captured operations where they are the right tool, but avoids generic dynamic
dispatch in hot containers. Its queue is an indexed ring worklist, not a
wrapper around Array.prototype.push.
This PoC deliberately does not lower arbitrary calls such as:
queue.push(item);
cache.get(key);
promise.then(fn);into internal operations.
Without strong receiver type information, that rewrite would be unsound. In robustness-zone code, the operation must be explicit instead of inferred.
Correctness tests verify that all runtimes produce the same normal observable results.
Negative tests verify that the unsafe baseline really fails after relevant prototype pollution. This is important: it proves the hardening solves a real failure mode rather than merely adding ceremony.
Robustness tests verify this sequence:
capture trusted operations
then pollute userland built-ins
then run selected internal paths
The pollution set includes:
Array.isArrayArray.prototype.reduceArray.prototype.pushArray.prototype.shiftArray.prototype.popArray.prototype.joinPromise.resolve
Additional boundary tests verify two non-claims:
- if pollution happens before trusted capture, protected variants may capture polluted operations;
- if an internal object is deliberately exposed to userland, userland can mutate it.
Pollution-sensitive scenarios are also executed in isolated child processes, so the check suite does not depend only on in-process descriptor restoration.
Robustness-zone files are checked for ambiguous dynamic method calls:
.push().pop().shift().unshift().slice().reduce().map().filter().join().get().set().then().catch().finally()
The checker supports an escape hatch:
// robustness-ignore-next-line: receiver is not a built-in container
customLogger.get("x");The checker is intentionally over-approximating. It is designed to catch ambiguous calls in PoC robustness-zone files, not to be used as-is across a large production codebase. A production version would need AST rules, per-zone allowlists, local suppressions, and better receiver analysis.
CI is used for correctness and robustness gates.
GitHub-hosted runners are also used for smoke benchmarks, but their numbers should not be treated as stable performance evidence. Stable performance numbers should be collected from pinned Node versions on known machines, preferably self-hosted runners, with CPU and OS metadata recorded in the report.
Recommended validation levels:
npm run checkon every commit.npm run bench:smokeon every pull request.npm run bench:guardafter smoke or perf benchmark runs.npm run bench:perfmanually on self-hosted machines.- Compare reports across Node 20, 22, and 24.
The GitHub Actions workflows follow that split:
.github/workflows/ci.ymlruns correctness checks, smoke benchmarks, and the soft guard across Linux, macOS, Windows, and Node 20/22/24..github/workflows/benchmark.ymlis a manual benchmark workflow for full benchmark runs on GitHub-hosted runners..github/workflows/perf-lab.ymlis a manual self-hosted runner workflow for controlled performance lab runs.
GitHub-hosted benchmark artifacts are useful for catching obvious breakage and large regressions. They are not used as final performance claims.
The benchmark guard always checks polluted-environment pass/fail behavior. It
only enforces slowdown thresholds for npm run bench:ci, npm run bench:perf,
or when BENCH_GUARD_STRICT=1 is set, so smoke runs do not fail on timing
noise.
The main benchmark refuses to run until verification passes.
Metadata recorded in reports/latest.md and reports/latest.json:
- date
- Node, V8, and libuv versions
- platform and architecture
- OS release
- CPU model and count
- total memory
- power mode if known
- git commit
- GitHub SHA and run id when available
- GitHub runner name, OS, and architecture when available
- CI flag
- iterations, samples, warmup
Metrics:
- min
- median
- mean
- p75
- p95
- p99
- max
- standard deviation
- slowdown vs baseline
Benchmark workloads:
sum-numbers: repeatedruntime.sumNumbers.queue: internal queue push/shift.simple-join: diagnostic formatting.mixed: queue plus diagnostic formatting.promise: promise resolution path.polluted:mixed: mixed workload after monkey-patching; baseline is expected to fail and is excluded from slowdown.
These results should not be interpreted as Node-wide performance claims. They compare cost models under this benchmark harness on one machine.
Generated by npm run bench on this machine:
| Environment | Workload | Runtime | Median ms | p75 ms | p95 ms | p99 ms | Mean ms | Stddev ms | Slowdown | Status |
|---|---|---|---|---|---|---|---|---|---|---|
| normal | sum-numbers | 01-baseline | 0.626 | 0.630 | 0.730 | 1.287 | 0.641 | 0.124 | 1.000x | passed |
| normal | sum-numbers | 02-primordials | 0.626 | 0.629 | 0.670 | 0.757 | 0.615 | 0.068 | 0.999x | passed |
| normal | sum-numbers | 03-explicit-contract | 0.531 | 0.533 | 0.550 | 1.074 | 0.536 | 0.070 | 0.848x | passed |
| normal | queue | 01-baseline | 0.046 | 0.046 | 0.057 | 0.088 | 0.047 | 0.007 | 1.000x | passed |
| normal | queue | 02-primordials | 0.047 | 0.048 | 0.053 | 0.092 | 0.049 | 0.007 | 1.040x | passed |
| normal | queue | 03-explicit-contract | 0.018 | 0.018 | 0.019 | 0.032 | 0.018 | 0.006 | 0.389x | passed |
| normal | simple-join | 01-baseline | 0.059 | 0.061 | 0.090 | 0.115 | 0.061 | 0.012 | 1.000x | passed |
| normal | simple-join | 02-primordials | 0.062 | 0.063 | 0.099 | 0.114 | 0.064 | 0.012 | 1.044x | passed |
| normal | simple-join | 03-explicit-contract | 0.062 | 0.063 | 0.105 | 0.128 | 0.065 | 0.013 | 1.042x | passed |
| normal | mixed | 01-baseline | 0.046 | 0.047 | 0.050 | 0.100 | 0.047 | 0.008 | 1.000x | passed |
| normal | mixed | 02-primordials | 0.051 | 0.052 | 0.056 | 0.102 | 0.052 | 0.008 | 1.110x | passed |
| normal | mixed | 03-explicit-contract | 0.018 | 0.018 | 0.019 | 0.022 | 0.018 | 0.005 | 0.387x | passed |
| normal | promise | 01-baseline | 0.037 | 0.041 | 0.074 | 0.085 | 0.040 | 0.015 | 1.000x | passed |
| normal | promise | 02-primordials | 0.048 | 0.049 | 0.082 | 0.088 | 0.047 | 0.014 | 1.295x | passed |
| normal | promise | 03-explicit-contract | 0.045 | 0.048 | 0.076 | 0.082 | 0.047 | 0.012 | 1.208x | passed |
| polluted | mixed | 01-baseline | n/a | n/a | n/a | n/a | n/a | n/a | n/a | failed |
| polluted | mixed | 02-primordials | 0.052 | 0.053 | 0.056 | 0.116 | 0.054 | 0.020 | n/a | passed |
| polluted | mixed | 03-explicit-contract | 0.018 | 0.018 | 0.020 | 0.030 | 0.018 | 0.011 | n/a | passed |
The key result is not that captured intrinsics are magically faster. They are not.
In this benchmark, 03-explicit-contract is faster than baseline in workloads
where the baseline relies on generic dynamic methods and the explicit variant
uses specialized internal structures. The queue does not call
Array.prototype.push, shift, or pop at all. It uses indexed storage with
explicit read/write state, so the hot path has less generic JavaScript semantics
to preserve.
The point is not that primordials are wrong. The point is that robustness should be scoped, explicit, tested, and benchmarked.
| Criterion | Primordials | Explicit-contract |
|---|---|---|
| Protection from prototype pollution | yes | yes, in selected zones |
| Manual coding tax | high and pervasive if used broadly | scoped; can be lint/autofix-assisted |
| Hot queue path | generic captured methods | specialized indexed container |
| Whole-program inference | not needed | not needed |
| Realm boundary | no | no |
| Sandbox | no | no |
| Node-wide replacement | possible partially | not claimed |
This PoC protects selected internal paths from accidental or incidental prototype pollution after trusted bootstrap.
It aims to handle:
- userland modifying
Array.prototype.push; - userland modifying
Array.prototype.reduce; - userland modifying
Array.prototype.join; - userland modifying
Promise.resolve; - userland modifying selected built-in prototype methods after bootstrap.
It does not aim to handle:
- malicious code with arbitrary access to internal objects;
- pollution before trusted bootstrap capture;
- sandbox escape prevention;
- protecting values deliberately exposed to userland;
- changing literal prototypes without a separate realm;
- full JavaScript semantic isolation.
-
Node.js primordials documentation:
doc/contributing/primordials.md
Describes Node.js coreprimordials, their purpose, usage patterns, and performance/readability caveats. -
nodejs/node#18795: Should internal modules be affected by monkey-patching?
Early discussion about whether Node.js internal modules should observe userland monkey-patching of built-in prototypes. -
nodejs/node#30697: Migration of core modules to primordials
Tracking issue for migrating Node.js core modules toward primordial-style internal operations. -
nodejs/TSC#1438: Removing
primordialsfrom Node.js project
Discussion of the readability, contributor-experience, optimization, and maintenance costs of pervasive primordials. -
nodejs/TSC#1439: New Strategic Initiative on Primordials
Strategic initiative proposing a clearer robustness model, use cases, CI tests, and guidance for where primordials should or should not be required.
MIT