fix(runtime): native ctor-class exports expose a real .prototype (#5268)#5269
Conversation
Native-module constructor *class* exports (`fs.ReadStream`, `fs.WriteStream`, `events.EventEmitter`, …) were truthy callable closures (bound-native exports) whose `.prototype` resolved to `undefined`: `ordinary_function_prototype_value_for_read` short-circuited to `None` for every bound-native export except a hardcoded http/https whitelist. So graceful-fs's `ReadStream.prototype = Object.create(fs$ReadStream.prototype)` (it guards on truthiness of `fs.ReadStream`, then reads `.prototype`), fs-extra (re-exports graceful-fs), and pino's `Object.setPrototypeOf(prototype, EventEmitter.prototype)` all passed `undefined` into `Object.create` / `Object.setPrototypeOf` and threw `TypeError: Object prototype may only be an Object or null: undefined` at module init — the #5268 corpus top pattern across 3 packages. Fix: replace the http/https whitelist with a principled rule — a bound-native export is a constructor class when its method name uses Node's constructor-cased convention (leading uppercase ASCII letter) AND it isn't flagged non-constructable. Such exports then flow through the existing synthetic-class path, which materializes a stable `.prototype` object. Non-constructor bound methods (`fs.readFile`, `path.join`, …) keep `prototype === undefined`, matching Node's built-in non-constructors. Regression tests: crates/perry/tests/issue_5268_*.rs (compile+run) and test-files/test_issue_5268_*.ts. graceful-fs / fs-extra now run; pino clears this throw and hits an unrelated later wall.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughReplaces a hard-coded allowlist of specific Node constructor ChangesNative constructor
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
#5268 —
Object prototype may only be an Object or null: undefined(corpus top pattern: graceful-fs, fs-extra, pino)Traced root cause
Native-module constructor class exports (
fs.ReadStream,fs.WriteStream,events.EventEmitter, …) are truthy callable closures (bound-native exports) whose.prototyperesolved toundefined.ordinary_function_prototype_value_for_read(crates/perry-runtime/src/object/class_registry.rs) short-circuited toNonefor every bound-native export except a hardcodedhttp/httpswhitelist (added for #3527/#4973). For everything else,.prototyperead backundefined.The three packages all guard on truthiness then read
.prototype:graceful-fs.js:242:var fs$ReadStream = fs.ReadStream; if (fs$ReadStream) { ReadStream.prototype = Object.create(fs$ReadStream.prototype) }→Object.create(undefined).lib/proto.js:77:Object.setPrototypeOf(prototype, EventEmitter.prototype)→Object.setPrototypeOf(x, undefined).Each fed
undefinedinto the proto-validation throw site.How it was traced
Temporary
eprintln!+ backtrace at the three throw sites (object_ops.rssetPrototypeOf,descriptors.rsObject.create,proxy/prototype.rsReflect). Running the real corpus drivers showed:js_object_create_with_propswithproto = undefined(and propsundefined).js_object_set_prototype_ofwithproto = undefined, receiver a real object.Reduced to
fs.ReadStream.prototype === undefined/EventEmitter.prototype === undefined, then localized to the bound-native whitelist gate. All tracing removed before commit.Fix
Replace the http/https whitelist with a principled rule: a bound-native export is a constructor class when its method name uses Node's constructor-cased convention (leading uppercase ASCII letter, e.g.
ReadStream/EventEmitter/Server) and it isn't flagged non-constructable (built-in prototype methods likeString.prototype.charAtcarry that flag). Such exports flow through the existing synthetic-class path (ensure_function_prototype_object), which materializes a stable.prototypeobject. Non-constructor bound methods (fs.readFile,path.join, …) keepprototype === undefined, matching Node's built-in non-constructor functions.One file changed:
crates/perry-runtime/src/object/class_registry.rs.Tests
crates/perry/tests/issue_5268_native_ctor_prototype_undefined.rs(compile+run) — passes.test-files/test_issue_5268_native_ctor_prototype_undefined.ts.test-files/test_issue_pino_prototype_undefined.ts(it claimedEventEmitter.prototypestays undefined; now it's a real object — the test's lenient assertions still pass).cargo test -p perry-runtime --lib -- --test-threads=1: 1044 passed, 0 failed. (Parallel runs surface a nondeterministic set of pre-existing shared-global-side-table flakes —url path_to_file_url,closure dynamic_props tests_1802, etc. — independent of this change; verified by stashing the fix and observing a different failing set on clean baseline parallel runs.)issue_4908_subclass_native_member_base,issue_5257_require_adopt_no_default_namespace, existingtest_gap_3527_http_ctor_prototype+test_issue_pino_prototype_undefined: pass.3-package before/after (real corpus,
PERRY_NO_AUTO_OPTIMIZE=1)graceful_fs_.tsObject prototype … : undefinedgraceful-fs object✅fs_extra_.tsObject prototype … : undefinedfs-extra object✅ (benignfs.realpath.nativewarning)pino_.tsObject prototype … : undefinedTypeError: Cannot read properties of undefined (reading 'get')(unrelated, not fixed here)Risks
Low. The change widens which bound-native exports get a synthesized
.prototypefrom a 6-name whitelist to "constructor-cased, constructable". This only affects.prototypereads on native exports (no behavior change for user closures, declared classes, or http/https which remain covered). Non-constructable exports are now explicitly excluded earlier, so built-in prototype methods are unaffected.Note for maintainer
No
Cargo.tomlversion /CLAUDE.md/CHANGELOG.md/Cargo.lockedits (per workflow — fold in at merge).Summary by CodeRabbit
Release Notes
Bug Fixes
.prototypeobjects at runtime, enabling valid inheritance patterns and prototype chain manipulation operations that previously failed with undefined values.Tests