1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
// @trace REQ-ENG-001 [entity:BaoRuntime]
use ::std::cell::RefCell;
use ::std::collections::HashSet;
use ::std::ptr::NonNull;
use bun_core::ZBox;
use mozjs::jsapi::*;
use mozjs::jsval::{JSVal, ObjectValue, UndefinedValue};
use mozjs::rooted;
/// GC-safe module cache: stores cached objects as properties on the JS global.
/// SpiderMonkey's GC manages these naturally — no raw pointer caching needed.
/// We only track which keys are set (a HashSet of strings).
///
/// Per-struct namespacing: keys are formatted as `__gc_{namespace}_{key}`
/// so different structs (ServerUserData, BaoTimeoutObject, EmitterState, etc.)
/// never collide even if they use the same local key (e.g. "handler").
struct GcStore {
keys: HashSet<String>,
}
impl GcStore {
fn new() -> Self {
GcStore {
keys: HashSet::new(),
}
}
/// Format a namespaced property name: `__gc_{namespace}_{key}`.
/// If namespace is empty, falls back to `__gc_cache_{key}` for backward compat.
fn prop_name(namespace: &str, key: &str) -> ZBox {
if namespace.is_empty() {
ZBox::from_vec(format!("__gc_cache_{}", key).into_bytes())
} else {
ZBox::from_vec(format!("__gc_{}_{}", namespace, key).into_bytes())
}
}
/// Full tracking key: `namespace::key` or just `key` if namespace is empty.
fn tracking_key(namespace: &str, key: &str) -> String {
if namespace.is_empty() {
key.to_string()
} else {
format!("{}::{}", namespace, key)
}
}
fn insert(&mut self, cx: *mut JSContext, namespace: &str, key: &str, obj: *mut JSObject) {
if obj.is_null() {
return;
}
let global = unsafe { CurrentGlobalOrNull(cx) };
if global.is_null() {
return;
}
let cx_ref = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
rooted!(&in(cx_ref) let global_root = global);
rooted!(&in(cx_ref) let obj_val_root = ObjectValue(obj));
let prop_name = Self::prop_name(namespace, key);
unsafe {
JS_DefineProperty(
cx,
global_root.handle().into(),
prop_name.as_ptr(),
obj_val_root.handle().into(),
(JSPROP_READONLY) as u32,
);
}
self.keys.insert(Self::tracking_key(namespace, key));
}
fn get(&self, cx: *mut JSContext, namespace: &str, key: &str) -> Option<*mut JSObject> {
if !self.keys.contains(&Self::tracking_key(namespace, key)) {
return None;
}
let global = unsafe { CurrentGlobalOrNull(cx) };
if global.is_null() {
return None;
}
let cx_ref = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
rooted!(&in(cx_ref) let global_root = global);
let prop_name = Self::prop_name(namespace, key);
let mut val = UndefinedValue();
unsafe {
JS_GetProperty(
cx,
global_root.handle().into(),
prop_name.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
);
}
if val.is_object() {
Some(val.to_object())
} else {
None
}
}
fn remove(&mut self, cx: *mut JSContext, namespace: &str, key: &str) {
if !self.keys.remove(&Self::tracking_key(namespace, key)) {
return;
}
let global = unsafe { CurrentGlobalOrNull(cx) };
if global.is_null() {
return;
}
let cx_ref = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
rooted!(&in(cx_ref) let global_root = global);
let prop_name = Self::prop_name(namespace, key);
unsafe {
JS_DeleteProperty1(cx, global_root.handle().into(), prop_name.as_ptr());
}
}
}
thread_local! {
static GC_STORE: RefCell<GcStore> = RefCell::new(GcStore::new());
}
/// Store a JSObject in the GC-safe store under a simple key.
/// The object is set as a property on the JS global, so SpiderMonkey's GC
/// manages it naturally. Uses `__gc_cache_{key}` as the property name.
pub fn gc_store_insert(cx: *mut JSContext, key: &str, obj: *mut JSObject) {
GC_STORE.with(|s| {
s.borrow_mut().insert(cx, "", key, obj);
});
}
/// Retrieve a JSObject from the GC-safe store by key.
/// Returns None if the key is not tracked or the global is unavailable.
///
/// # GC rooting contract (BCE — dangling-nursery-pointer SIGSEGV class)
///
/// The returned `*mut JSObject` is a BARE pointer into the GC heap — the
/// cached object may live in the NURSERY, where a minor GC MOVES it (the
/// from-space address is then zeroed). Callers MUST pass it through
/// `rooted!(&in(cx) let r = p)` (or otherwise root it) BEFORE any operation
/// that can allocate or run JS (`JS_New*`, `JS::Evaluate*`, `JS_Call*`,
/// `JS_Define*`, `JS_SetProperty`, getter-capable `JS_GetProperty` on
/// non-plain objects). Rooting after such an operation captures an already
/// stale address and protects nothing. Same contract for
/// [`gc_store_get_ns`] and [`crate::require::get_builtin`]. Live-gdb
/// evidence: worker-realm install crashed in `JSObject::getOpsGetProperty`
/// (si_addr=0) after `JS::Evaluate2` moved the promises singleton —
/// c12/c16/c17 under `dom_webgl2_enabled`.
pub fn gc_store_get(cx: *mut JSContext, key: &str) -> Option<*mut JSObject> {
GC_STORE.with(|s| s.borrow().get(cx, "", key))
}
/// Remove a JSObject from the GC-safe store by key.
/// Deletes the property from the JS global and removes the tracking key.
pub fn gc_store_remove(cx: *mut JSContext, key: &str) {
GC_STORE.with(|s| {
s.borrow_mut().remove(cx, "", key);
});
}
/// Store a JSObject in the GC-safe store under a namespaced key.
/// The object is set as a property on the JS global. `namespace` prevents
/// key collisions between structs (e.g., `"ServerUserData"` vs `"EmitterState"`).
/// Property name format: `__gc_{namespace}_{key}`.
pub fn gc_store_insert_ns(cx: *mut JSContext, namespace: &str, key: &str, obj: *mut JSObject) {
GC_STORE.with(|s| {
s.borrow_mut().insert(cx, namespace, key, obj);
});
}
/// Retrieve a JSObject from the GC-safe store by namespaced key.
///
/// Same GC rooting contract as [`gc_store_get`]: root the returned bare
/// pointer before any allocation or JS execution.
pub fn gc_store_get_ns(cx: *mut JSContext, namespace: &str, key: &str) -> Option<*mut JSObject> {
GC_STORE.with(|s| s.borrow().get(cx, namespace, key))
}
/// Remove a JSObject from the GC-safe store by namespaced key.
pub fn gc_store_remove_ns(cx: *mut JSContext, namespace: &str, key: &str) {
GC_STORE.with(|s| {
s.borrow_mut().remove(cx, namespace, key);
});
}
/// Generate a namespaced GcStore key. Format: `"__gc_{namespace}_{id}"`.
/// Use this to avoid key collisions between different modules storing objects
/// in the global GcStore (e.g., `"http_server_1_handler"` vs `"timer_cb_42"`).
/// Crate-internal key formatter (the `__gc_{ns}_{id}` shape): only
/// `gc_store_unique_key` consumes it now — the historical external callers
/// were replaced by the `_ns` accessors during BUG-ENG-360. Dead-link
/// triage: visibility narrowed from `pub` to match the real surface.
pub(crate) fn gc_store_key(namespace: &str, id: u64) -> String {
format!("__gc_{}_{}", namespace, id)
}
/// Atomic counter for generating unique GcStore keys.
use ::std::sync::atomic::{AtomicU64, Ordering};
static GC_KEY_COUNTER: AtomicU64 = AtomicU64::new(1);
/// Generate a unique GcStore key with auto-incrementing ID.
/// Format: `"__gc_{namespace}_{auto_id}"`.
pub fn gc_store_unique_key(namespace: &str) -> String {
let id = GC_KEY_COUNTER.fetch_add(1, Ordering::Relaxed);
gc_store_key(namespace, id)
}
// ── Unit tests ──
#[cfg(test)]
mod tests {
use super::*;
use bun_core::ByteSlice;
#[test]
fn prop_name_empty_namespace_uses_cache_prefix() {
let c = GcStore::prop_name("", "foo");
let s = c.to_str().unwrap();
assert_eq!(s, "__gc_cache_foo");
}
#[test]
fn prop_name_with_namespace() {
let c = GcStore::prop_name("ServerUserData", "handler");
let s = c.to_str().unwrap();
assert_eq!(s, "__gc_ServerUserData_handler");
}
#[test]
fn tracking_key_empty_namespace() {
assert_eq!(GcStore::tracking_key("", "foo"), "foo");
}
#[test]
fn tracking_key_with_namespace() {
assert_eq!(
GcStore::tracking_key("EmitterState", "data:0"),
"EmitterState::data:0"
);
}
#[test]
fn tracking_key_uniqueness() {
let a = GcStore::tracking_key("ServerUserData", "handler");
let b = GcStore::tracking_key("BunServeUserData", "handler");
assert_ne!(a, b, "same key in different namespaces must be distinct");
}
#[test]
fn gc_store_new_is_empty() {
let store = GcStore::new();
assert!(store.keys.is_empty());
}
#[test]
fn gc_store_key_format() {
assert_eq!(gc_store_key("timer", 42), "__gc_timer_42");
}
#[test]
fn gc_store_unique_key_format() {
let k1 = gc_store_unique_key("http");
let k2 = gc_store_unique_key("http");
assert!(k1.starts_with("__gc_http_"));
assert!(k2.starts_with("__gc_http_"));
// IDs should be different
assert_ne!(k1, k2);
}
#[test]
fn gc_store_unique_key_counter_increments() {
// Use fetch_add return value instead of global counter comparison,
// since other test threads also increment the shared AtomicU64.
let before = GC_KEY_COUNTER.fetch_add(0, Ordering::SeqCst);
let _ = gc_store_unique_key("test_ns");
let after = GC_KEY_COUNTER.fetch_add(0, Ordering::SeqCst);
assert!(
after > before,
"counter must increment: before={before}, after={after}"
);
}
}