-
Notifications
You must be signed in to change notification settings - Fork 5k
node builtins: defer cross-module requires and load-time work #35541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
405d205
a30b8a6
15a5913
946b625
ec4868a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,17 @@ function uncurryThis(func) { | |
| return FunctionPrototypeCall.bind(func); | ||
| } | ||
|
|
||
| const ArrayPrototypeForEach = uncurryThis(Array.prototype.forEach); | ||
| const ArrayPrototypeMap = uncurryThis(Array.prototype.map); | ||
| const ArrayPrototypeSymbolIterator = uncurryThis(Array.prototype[Symbol.iterator]); | ||
| const ArrayIteratorPrototypeNext = uncurryThis(Array.prototype[Symbol.iterator]().next); | ||
| const StringPrototypeSymbolIterator = uncurryThis(String.prototype[Symbol.iterator]); | ||
| const StringIteratorPrototypeNext = uncurryThis( | ||
| Reflect.getPrototypeOf(String.prototype[Symbol.iterator].$call("")).next, | ||
| ); | ||
| const PromiseAll = Promise.all; | ||
| const PromiseResolve = Promise.$resolve.bind(Promise); | ||
|
|
||
| const copyProps = (src, dest) => { | ||
| ArrayPrototypeForEach(Reflect.ownKeys(src), key => { | ||
| if (!Reflect.getOwnPropertyDescriptor(dest, key)) { | ||
|
|
@@ -77,26 +88,23 @@ const makeSafe = (unsafe, safe) => { | |
| return safe; | ||
| }; | ||
|
|
||
| const StringIterator = uncurryThis(String.prototype[Symbol.iterator]); | ||
| const StringIteratorPrototype = Reflect.getPrototypeOf(StringIterator("")); | ||
| const ArrayPrototypeForEach = uncurryThis(Array.prototype.forEach); | ||
| const ArrayPrototypeSymbolIterator = uncurryThis(Array.prototype[Symbol.iterator]); | ||
| const ArrayIteratorPrototypeNext = uncurryThis(Array.prototype[Symbol.iterator]().next); | ||
| const SafeArrayIterator = createSafeIterator(ArrayPrototypeSymbolIterator, ArrayIteratorPrototypeNext); | ||
| let SafeArrayIterator; | ||
| function getSafeArrayIterator() { | ||
| return (SafeArrayIterator ??= createSafeIterator(ArrayPrototypeSymbolIterator, ArrayIteratorPrototypeNext)); | ||
| } | ||
|
|
||
| const ArrayPrototypeMap = Array.prototype.map; | ||
| const PromisePrototypeThen = $Promise.prototype.$then; | ||
|
|
||
| const arrayToSafePromiseIterable = (promises, mapFn) => | ||
| new SafeArrayIterator( | ||
| ArrayPrototypeMap.$call( | ||
| const arrayToSafePromiseIterable = (promises, mapFn) => { | ||
| const SafeArrayIterator = getSafeArrayIterator(); | ||
| return new SafeArrayIterator( | ||
| ArrayPrototypeMap( | ||
| promises, | ||
| (promise, i) => | ||
| new Promise((a, b) => PromisePrototypeThen.$call(mapFn == null ? promise : mapFn(promise, i), a, b)), | ||
| ), | ||
| ); | ||
| const PromiseAll = Promise.all; | ||
| const PromiseResolve = Promise.$resolve.bind(Promise); | ||
| }; | ||
| const SafePromiseAll = (promises, mapFn) => PromiseAll(arrayToSafePromiseIterable(promises, mapFn)); | ||
| // Shared scheduler for SafePromiseAllReturnVoid/ReturnArrayLike: `returnVal` | ||
| // is null for the void variant (no result bookkeeping, resolves with nothing). | ||
|
|
@@ -126,64 +134,87 @@ const SafePromiseAllReturnArrayLike = (promises, mapFn) => { | |
| return safePromiseAllCollect(promises, mapFn, returnVal); | ||
| }; | ||
|
|
||
| export default { | ||
| const primordials = { | ||
| Array, | ||
| SafeArrayIterator, | ||
| MapPrototypeGetSize: getGetter(Map, "size"), | ||
| Number, | ||
| Object, | ||
| RegExp, | ||
| SafeStringIterator: createSafeIterator(StringIterator, uncurryThis(StringIteratorPrototype.next)), | ||
| SafeMap: makeSafe( | ||
| SafePromiseAll, | ||
| SafePromiseAllReturnArrayLike, | ||
| SafePromiseAllReturnVoid, | ||
| String, | ||
| Uint8ClampedArray, | ||
| Uint8Array, | ||
| Uint16Array, | ||
| Uint32Array, | ||
| Int8Array, | ||
| Int16Array, | ||
| Int32Array, | ||
| Float16Array, | ||
| Float32Array, | ||
| Float64Array, | ||
| BigUint64Array, | ||
| BigInt64Array, | ||
| uncurryThis, | ||
| }; | ||
|
|
||
| function defineLazy(name, initialize) { | ||
| Object.defineProperty(primordials, name, { | ||
| get() { | ||
| const value = initialize(); | ||
| Reflect.defineProperty(primordials, name, { value, writable: true, enumerable: true, configurable: true }); | ||
| return value; | ||
| }, | ||
| enumerable: true, | ||
| configurable: true, | ||
| }); | ||
| } | ||
|
|
||
| defineLazy("SafeArrayIterator", getSafeArrayIterator); | ||
| defineLazy("MapPrototypeGetSize", () => getGetter(Map, "size")); | ||
| defineLazy("SetPrototypeGetSize", () => getGetter(Set, "size")); | ||
| defineLazy("TypedArrayPrototypeGetLength", () => getGetter(Uint8Array, "length")); | ||
| defineLazy("TypedArrayPrototypeGetSymbolToStringTag", () => getGetter(Uint8Array, Symbol.toStringTag)); | ||
| defineLazy("SafeStringIterator", () => createSafeIterator(StringPrototypeSymbolIterator, StringIteratorPrototypeNext)); | ||
| defineLazy("SafeMap", () => | ||
| makeSafe( | ||
| Map, | ||
| class SafeMap extends Map { | ||
| constructor(i) { | ||
|
Comment on lines
+172
to
183
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Wrapping Extended reasoning...What changed
Why this widens the tamper window (narrowly)Previously, loading
Addressing the refutationOne verifier argued this is not a real regression because (a) Bun's primordials was never bootstrap-loaded, so tamper-resistance was always load-order-dependent, and (b) every consumer destructures at its own module top-level, firing the lazy getter synchronously during that same Point (a) is correct and important: this file was never a hard tamper boundary. Pre-PR, user code that tampered Point (b) is mostly right but not universally: Step-by-step example
Pre-PR, step 1 would have snapshotted Why nit, not normal
Suggested cheap mitigation (optional)Capture the raw prototypes into module-level consts alongside the iterator inputs already at lines 39-46 — e.g.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified the timing: at HEAD nothing loads internal/primordials before user code either (only lazy require sites in on-demand modules; no bootstrap requireId), and a Map.prototype.size tamper on line 1 of a script is already visible to MapPrototypeGetSize on release 1.4.0 — so the snapshot was always post-user-code; this only moves it from first-module-require to first-property-access. Where a module actually depends on capturing at its own load (assert's SafeMap/SafeSet/SafeWeakSet, readline's SafeStringIterator, worker_threads/messaging's SafeMap) the destructure stays at module top level. Left as-is. |
||
| super(i); | ||
| } | ||
| }, | ||
| ), | ||
| SafePromiseAll, | ||
| SafePromiseAllReturnArrayLike, | ||
| SafePromiseAllReturnVoid, | ||
| SafeSet: makeSafe( | ||
| ); | ||
| defineLazy("SafeSet", () => | ||
| makeSafe( | ||
| Set, | ||
| class SafeSet extends Set { | ||
| constructor(i) { | ||
| super(i); | ||
| } | ||
| }, | ||
| ), | ||
| SafeWeakSet: makeSafe( | ||
| ); | ||
| defineLazy("SafeWeakSet", () => | ||
| makeSafe( | ||
| WeakSet, | ||
| class SafeWeakSet extends WeakSet { | ||
| constructor(i) { | ||
| super(i); | ||
| } | ||
| }, | ||
| ), | ||
| SafeWeakMap: makeSafe( | ||
| ); | ||
| defineLazy("SafeWeakMap", () => | ||
| makeSafe( | ||
| WeakMap, | ||
| class SafeWeakMap extends WeakMap { | ||
| constructor(i) { | ||
| super(i); | ||
| } | ||
| }, | ||
| ), | ||
| SetPrototypeGetSize: getGetter(Set, "size"), | ||
| String, | ||
| TypedArrayPrototypeGetLength: getGetter(Uint8Array, "length"), | ||
| TypedArrayPrototypeGetSymbolToStringTag: getGetter(Uint8Array, Symbol.toStringTag), | ||
| Uint8ClampedArray, | ||
| Uint8Array, | ||
| Uint16Array, | ||
| Uint32Array, | ||
| Int8Array, | ||
| Int16Array, | ||
| Int32Array, | ||
| Float16Array, | ||
| Float32Array, | ||
| Float64Array, | ||
| BigUint64Array, | ||
| BigInt64Array, | ||
| uncurryThis, | ||
| }; | ||
| ); | ||
|
|
||
| export default primordials; | ||
Uh oh!
There was an error while loading. Please reload this page.