From 7d1f9580eeebf3b73350575bd4f9cfe8deb51967 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:55:52 -0500 Subject: [PATCH] Release path module members through the object core's accessors The exit-time cleanup of the path module objects swapped each member value out of the object's storage by reading v.obj.slots and v.obj.entries directly. It now goes through scr_dyn_obj_count and a new scr_dyn.h accessor, scr_dyn_obj_exchange_value_at, which stores a value at an index without reading the keys, so the cleanup still runs after the exit-time layout reset. --- packages/runtime/src/scr_dyn.h | 11 +++++++++++ packages/runtime/src/scr_node_builtin.c | 12 ++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 47f9046b6b..839e654f04 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -770,6 +770,17 @@ static inline __attribute__((always_inline)) ScrDynProp scr_dyn_obj_prop_at(cons e->accessor, e->writable, e->enumerable, e->configurable}; } +/* Stores `value` (moved) as the value of data property i and returns the + * previous value (owned by the caller). Reads no keys and does not follow + * the layout pointer, so exit-time teardown can use it after the layouts + * are freed. */ +static inline ScrDyn *scr_dyn_obj_exchange_value_at(ScrDyn *o, size_t i, ScrDyn *value) { + ScrDyn **slot = o->v.obj.layout ? &o->v.obj.slots[i] : &o->v.obj.entries[i].value; + ScrDyn *previous = *slot; + *slot = value; + return previous; +} + /* The index of own property `key`, or SIZE_MAX. A linear scan in * dictionary mode: for code that looks up options and headers, not for hot * property reads. */ diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 71b523742c..d312a6e805 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -368,18 +368,14 @@ static void scr_builtin_release(void *handle) { static void scr_builtin_cleanup(void) { /* An exports object can hold itself and the other path family * (path.posix.posix, path.posix.win32): its member values are released - * first, so the objects are freed. The values are swapped out of the - * object's own storage, without reading its keys: the exit-time layout + * first, so the objects are freed. The values are swapped out without + * reading the keys (scr_dyn_obj_exchange_value_at): the exit-time layout * reset can run before this and free the layout the keys live in. */ for (ScrBuiltinModule *module = scr_builtin_modules; module; module = module->next) { if (!module->object) continue; ScrDyn *exports = module->exports; - for (size_t i = 0; i < exports->v.obj.len; i++) { - ScrDyn **slot = exports->v.obj.layout ? &exports->v.obj.slots[i] : &exports->v.obj.entries[i].value; - ScrDyn *value = *slot; - *slot = scr_dyn_retain(scr_dyn_undefined()); - scr_dyn_release(value); - } + for (size_t i = 0; i < scr_dyn_obj_count(exports); i++) + scr_dyn_release(scr_dyn_obj_exchange_value_at(exports, i, scr_dyn_retain(scr_dyn_undefined()))); } while (scr_builtin_modules) { ScrBuiltinModule *module = scr_builtin_modules;