rusty_racer 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +13 -8
- data/ext/rusty_racer/Cargo.toml +2 -2
- data/ext/rusty_racer/src/lib.rs +85 -62
- data/ext/rusty_racer/src/stack.rs +241 -84
- data/lib/rusty_racer/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bef5f6ca0708cb89d0468f4aaca77ccdae184114733de79980b2a84c51660cb3
|
|
4
|
+
data.tar.gz: 843f938abbb31aeb8631bb943052d3b326990eee1e13d38537aecba4c409a7b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db1ae8f6a5e6793b5ccb172d9412b94e70535e4b4adb25ba18ba8128bdee006ed29ce87a3de60c4aadaaf497239a27655ae1d9573dad3b4f8b764f12601c7bee
|
|
7
|
+
data.tar.gz: ed7e48d5c2e942cfbe368e1c61ca7169ef4c126a609e4f9ec189e73f6534946ebeb91e48e284c3965547d6437cdbf1819c6e0498e781adf151d286d1054a4422
|
data/README.md
CHANGED
|
@@ -252,14 +252,19 @@ In-thread V8 runs on whatever stack the calling Ruby code is on — including a
|
|
|
252
252
|
**Fiber**'s separate stack (a plain `Enumerator` is a Fiber, so this is common:
|
|
253
253
|
`Capybara::Result#find`, lazy enumerators, …). This works on the **main thread**,
|
|
254
254
|
where the process stack is the highest address and every Fiber sits below it.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
main thread
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
255
|
+
That includes a Fiber entered *underneath* a running JS call — a host function
|
|
256
|
+
that resumes a Fiber which evals again — since each operation describes its own
|
|
257
|
+
stack to V8 and restores the enclosing one when it returns.
|
|
258
|
+
|
|
259
|
+
On a **non-main thread** it does not. V8 decides "is this the central stack?" by
|
|
260
|
+
testing the stack pointer against a window that ends at the thread's native stack
|
|
261
|
+
top — a pthread value it caches, with no API to retarget on POSIX. Only the
|
|
262
|
+
window's *lower* edge follows the per-operation stack limit, so a Fiber allocated
|
|
263
|
+
*above* that top — the usual case off the main thread, whose own stack sits below
|
|
264
|
+
later Fiber mmaps — is outside the window whatever we do, and V8 aborts the
|
|
265
|
+
process on the next GC or thrown exception. So **don't call into an isolate from
|
|
266
|
+
inside a Fiber on a worker thread**; drive isolate ops directly on the thread, or
|
|
267
|
+
keep Fiber/Enumerator-mediated JS calls on the main thread.
|
|
263
268
|
|
|
264
269
|
## Installation
|
|
265
270
|
|
data/ext/rusty_racer/Cargo.toml
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# links and loads with no custom archive.
|
|
6
6
|
[package]
|
|
7
7
|
name = "rusty_racer"
|
|
8
|
-
version = "0.1.
|
|
8
|
+
version = "0.1.14"
|
|
9
9
|
edition = "2024"
|
|
10
10
|
publish = false
|
|
11
11
|
|
|
@@ -23,7 +23,7 @@ rb-sys = "0.9"
|
|
|
23
23
|
# ~6-8x faster marshalling for non-ASCII results (measured); ASCII is unaffected.
|
|
24
24
|
# The feature makes the v8 crate fetch Deno's _simdutf prebuilt archive variant
|
|
25
25
|
# (which is built with rusty_v8_enable_simdutf=true) on every platform.
|
|
26
|
-
v8 = { version = "150.
|
|
26
|
+
v8 = { version = "150.4.0", features = ["simdutf"] }
|
|
27
27
|
# pthread stack-bounds query, to set V8's stack limit per op relative to the
|
|
28
28
|
# current native thread (in-thread V8 runs on the Ruby thread's stack).
|
|
29
29
|
libc = "0.2"
|
data/ext/rusty_racer/src/lib.rs
CHANGED
|
@@ -54,7 +54,7 @@ use marshal::{js_to_jsval, jsval_to_js, jsval_to_ruby, ruby_to_jsval, JsVal};
|
|
|
54
54
|
mod ops;
|
|
55
55
|
use ops::{run_source, service_request, Compiled, Request, VmReply};
|
|
56
56
|
mod stack;
|
|
57
|
-
use stack::{current_real_isolate, discover_scan_start_field,
|
|
57
|
+
use stack::{current_real_isolate, discover_scan_start_field, StackScope, STACK_DEBUG};
|
|
58
58
|
mod watchdog;
|
|
59
59
|
use watchdog::{
|
|
60
60
|
arm_watchdog, disarm_watchdog, run_js_bracketed, watchdog_loop, WatchdogShared, WATCHDOG_DEBUG,
|
|
@@ -1229,10 +1229,16 @@ struct Core {
|
|
|
1229
1229
|
// the owner thread.
|
|
1230
1230
|
iso_ptr: IsoPtr,
|
|
1231
1231
|
// Address of V8's conservative-GC-scan stack_start field (see
|
|
1232
|
-
// discover_scan_start_field), or 0 if discovery failed.
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1232
|
+
// discover_scan_start_field), or 0 if discovery failed. StackScope writes the
|
|
1233
|
+
// fiber region top here per fiber op so a GC scan stays mapped. Set once at
|
|
1234
|
+
// creation, then read-only; AtomicUsize for shared &Core access.
|
|
1235
1235
|
scan_start_field: std::sync::atomic::AtomicUsize,
|
|
1236
|
+
// The stack limit currently installed in V8 for this isolate. StackScope
|
|
1237
|
+
// keeps it here because V8 exposes no getter and a nested op running on
|
|
1238
|
+
// another stack has to put the enclosing op's limit back. 0 before the first
|
|
1239
|
+
// op. Owner-thread only, like the isolate itself; AtomicUsize for shared
|
|
1240
|
+
// &Core access.
|
|
1241
|
+
installed_stack_limit: std::sync::atomic::AtomicUsize,
|
|
1236
1242
|
// Re-entry depth for THIS isolate, readable without a scope (the runner needs
|
|
1237
1243
|
// it to choose the scope kind before any scope exists): 0 = top-level op
|
|
1238
1244
|
// (open a fresh HandleScope from iso_ptr); >0 = a host callback is on the V8
|
|
@@ -1998,6 +2004,7 @@ impl Isolate {
|
|
|
1998
2004
|
_owner_root: RootedThread(BoxValue::new(owner_thread)),
|
|
1999
2005
|
iso_ptr,
|
|
2000
2006
|
scan_start_field: std::sync::atomic::AtomicUsize::new(0),
|
|
2007
|
+
installed_stack_limit: std::sync::atomic::AtomicUsize::new(0),
|
|
2001
2008
|
depth: std::sync::atomic::AtomicU32::new(0),
|
|
2002
2009
|
procs: Mutex::new(ProcTable::default()),
|
|
2003
2010
|
default_timeout_ms: timeout_ms,
|
|
@@ -2084,29 +2091,69 @@ impl Core {
|
|
|
2084
2091
|
// poison the isolate and raise instead of crashing the process.
|
|
2085
2092
|
use std::panic::AssertUnwindSafe;
|
|
2086
2093
|
let reply: Option<VmReply> = without_gvl(|| {
|
|
2087
|
-
|
|
2094
|
+
// iso_ptr is *mut v8::Isolate = *mut NonNull<RealIsolate>; the raw
|
|
2095
|
+
// v8::Isolate* the C++ entry points want is that NonNull's value.
|
|
2096
|
+
let real_isolate = unsafe { *(iso as *const *mut c_void) };
|
|
2097
|
+
// Whether this op has to ENTER the isolate itself. At depth 0 always:
|
|
2098
|
+
// between ops the isolate is exited, so each op enters around its run.
|
|
2099
|
+
//
|
|
2100
|
+
// A re-entrant op (a host callback or module resolver, having
|
|
2101
|
+
// reacquired the GVL to run a proc that issued this op, is on the V8
|
|
2102
|
+
// stack) USUALLY does not: the JS on the stack belongs to THIS
|
|
2103
|
+
// isolate, which is therefore already entered on THIS native thread,
|
|
2104
|
+
// and we bootstrap onto the ambient HandleScope instead.
|
|
2105
|
+
//
|
|
2106
|
+
// But an embedder driving MANY isolates (e.g. capybara-simulated's
|
|
2107
|
+
// windows) can interleave them: a host callback on isolate A runs
|
|
2108
|
+
// Ruby that evals isolate B, whose own callback re-enters A. Now A is
|
|
2109
|
+
// on the V8 stack (depth > 0) yet B — not A — is the isolate
|
|
2110
|
+
// CURRENTLY entered on this thread, so bootstrapping a scope on A and
|
|
2111
|
+
// opening a ContextScope would trip V8's "scope and Context do not
|
|
2112
|
+
// belong to the same Isolate" panic (it checks the scope's isolate
|
|
2113
|
+
// against Isolate::GetCurrent()). Detect that case and properly enter
|
|
2114
|
+
// A on top of B, restoring B on exit.
|
|
2115
|
+
let entered = depth == 0 || real_isolate != current_real_isolate();
|
|
2116
|
+
// Snapshot the ENCLOSING op's conservative-GC-scan start BEFORE
|
|
2117
|
+
// entering: Isolate::Enter re-points it at the native top, so reading
|
|
2118
|
+
// it afterwards would hand StackScope the clobbered value to
|
|
2119
|
+
// "restore" — stranding an enclosing FIBER op with a start on the
|
|
2120
|
+
// native stack, whose next GC walks off the fiber's mapped top and
|
|
2121
|
+
// SEGVs. None at depth 0, where the value in the field belongs to the
|
|
2122
|
+
// previous, already-finished op and so is nothing to preserve.
|
|
2123
|
+
let scan_start_field = self.scan_start_field.load(Ordering::Relaxed);
|
|
2124
|
+
let enclosing_scan_start = if depth > 0 && scan_start_field != 0 {
|
|
2125
|
+
Some(unsafe { *(scan_start_field as *const usize) })
|
|
2126
|
+
} else {
|
|
2127
|
+
None
|
|
2128
|
+
};
|
|
2129
|
+
if entered {
|
|
2088
2130
|
unsafe { (*iso).enter() };
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2131
|
+
}
|
|
2132
|
+
// In-thread: V8 runs on whatever stack the Ruby caller is on — this
|
|
2133
|
+
// native thread's, or a Ruby Fiber's if a host callback resumed one.
|
|
2134
|
+
// Describe that stack to the isolate for the duration of the op, and
|
|
2135
|
+
// put the enclosing op's description back afterwards. Both the
|
|
2136
|
+
// create-time limit (fixed at a shallow, possibly other-thread frame)
|
|
2137
|
+
// and an enclosing op's are stale here, and a stale limit doesn't
|
|
2138
|
+
// merely false-overflow — it makes the resulting throw FATAL. See
|
|
2139
|
+
// StackScope.
|
|
2140
|
+
//
|
|
2141
|
+
// stack_top_marker is a live address ABOVE every V8 frame of this op
|
|
2142
|
+
// (the scope and service_request below run in deeper frames); on a
|
|
2143
|
+
// fiber it becomes V8's conservative-GC-scan start so the scan stays
|
|
2144
|
+
// within the live, mapped stack.
|
|
2145
|
+
let stack_top_marker = 0u8;
|
|
2146
|
+
// Word-align (down): V8 CHECKs the scan start is pointer-aligned.
|
|
2147
|
+
// Down stays above all V8 frames (they're a full frame below).
|
|
2148
|
+
let stack_top = (&stack_top_marker as *const u8 as usize) & !(size_of::<usize>() - 1);
|
|
2149
|
+
let stack = StackScope::enter(
|
|
2150
|
+
real_isolate,
|
|
2151
|
+
scan_start_field,
|
|
2152
|
+
enclosing_scan_start,
|
|
2153
|
+
&self.installed_stack_limit,
|
|
2154
|
+
stack_top,
|
|
2155
|
+
);
|
|
2156
|
+
if depth == 0 {
|
|
2110
2157
|
let mut reply = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
|
2111
2158
|
v8::scope!(let scope, unsafe { &mut *iso });
|
|
2112
2159
|
service_request(scope, request, true)
|
|
@@ -2145,45 +2192,20 @@ impl Core {
|
|
|
2145
2192
|
istate!(iso_ref).oom_fired = false;
|
|
2146
2193
|
reply = reply.map(relabel_oom);
|
|
2147
2194
|
}
|
|
2195
|
+
// Drop the stack description BEFORE popping the isolate — exit()
|
|
2196
|
+
// can leave a different one current. At depth 0 the drop restores
|
|
2197
|
+
// nothing (no enclosing op), so this just releases the scope.
|
|
2198
|
+
drop(stack);
|
|
2148
2199
|
unsafe { (*iso).exit() };
|
|
2149
2200
|
reply
|
|
2150
2201
|
} else {
|
|
2151
|
-
// Re-entrant
|
|
2152
|
-
//
|
|
2153
|
-
//
|
|
2154
|
-
//
|
|
2155
|
-
//
|
|
2156
|
-
// HandleScope without re-entering.
|
|
2157
|
-
//
|
|
2158
|
-
// But an embedder driving MANY isolates (e.g. capybara-simulated's
|
|
2159
|
-
// windows) can interleave them: a host callback on isolate A runs
|
|
2160
|
-
// Ruby that evals isolate B, whose own callback re-enters A. Now A
|
|
2161
|
-
// is on the V8 stack (depth > 0) yet B — not A — is the isolate
|
|
2162
|
-
// CURRENTLY entered on this thread, so bootstrapping a scope on A
|
|
2163
|
-
// and opening a ContextScope would trip V8's "scope and Context do
|
|
2164
|
-
// not belong to the same Isolate" panic (it checks the scope's
|
|
2165
|
-
// isolate against Isolate::GetCurrent()). Detect that case and
|
|
2166
|
-
// properly enter A on top of B, restoring B on exit. Entering an
|
|
2167
|
-
// already-current isolate is also harmless (V8's entered-isolate
|
|
2168
|
-
// stack nests), so this is correct for same-isolate reentry too —
|
|
2169
|
-
// we only pay the enter/exit when a FOREIGN isolate is on top.
|
|
2170
|
-
//
|
|
2171
|
-
// The stack limit + scan-start set at depth 0 are NOT re-pointed
|
|
2172
|
-
// here: reentry runs in DEEPER frames of the SAME stack, so the
|
|
2173
|
-
// depth-0 values still bound it correctly (whichever isolate is
|
|
2174
|
-
// current — each tracks its own limit). The one exception is a
|
|
2175
|
-
// host callback that SWITCHES stacks — e.g. resumes a Ruby Fiber
|
|
2176
|
-
// that itself evals — where the depth-0 (native) settings are
|
|
2177
|
-
// stale for the fiber; that nested-fiber-under-callback case is an
|
|
2178
|
-
// unsupported edge (the realistic fiber path is a depth-0 eval).
|
|
2179
|
-
let foreign = unsafe { *(iso as *const *mut c_void) } != current_real_isolate();
|
|
2180
|
-
if foreign {
|
|
2181
|
-
unsafe { (*iso).enter() };
|
|
2182
|
-
}
|
|
2202
|
+
// Re-entrant. `entered` is true only when a FOREIGN isolate was
|
|
2203
|
+
// on top (see above): that one had no ambient scope of ours under
|
|
2204
|
+
// its entry, so it opens a fresh HandleScope exactly as the
|
|
2205
|
+
// depth-0 path does. Same-isolate reentry bootstraps onto the
|
|
2206
|
+
// ambient one instead.
|
|
2183
2207
|
let reply = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
|
2184
|
-
if
|
|
2185
|
-
// A had no ambient scope under B's entry — open a fresh
|
|
2186
|
-
// HandleScope on A, exactly as the depth-0 path does.
|
|
2208
|
+
if entered {
|
|
2187
2209
|
v8::scope!(let scope, unsafe { &mut *iso });
|
|
2188
2210
|
service_request(scope, request, false)
|
|
2189
2211
|
} else {
|
|
@@ -2195,7 +2217,8 @@ impl Core {
|
|
|
2195
2217
|
// Pop A back off (restoring B as current). Safe even after a
|
|
2196
2218
|
// panic unwind: the scope's Drop ran but left A entered, and
|
|
2197
2219
|
// exit() asserts A == GetCurrent(), which holds here.
|
|
2198
|
-
|
|
2220
|
+
drop(stack);
|
|
2221
|
+
if entered {
|
|
2199
2222
|
unsafe { (*iso).exit() };
|
|
2200
2223
|
}
|
|
2201
2224
|
reply
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// — no IsolateState/JsVal/marshalling. It also hosts current_real_isolate() (the
|
|
6
6
|
// entered-isolate query) since that too is just one of the exported V8 symbols
|
|
7
7
|
// below, kept here so the FFI block stays in one place. The crate uses
|
|
8
|
-
// discover_scan_start_field (once per isolate),
|
|
8
|
+
// discover_scan_start_field (once per isolate), StackScope (per op),
|
|
9
9
|
// current_real_isolate (per reentrant op), and STACK_DEBUG (set at init);
|
|
10
10
|
// everything else is private to this module.
|
|
11
11
|
|
|
@@ -14,7 +14,7 @@ use std::ffi::c_void;
|
|
|
14
14
|
// don't warn it unused.
|
|
15
15
|
#[cfg(target_os = "linux")]
|
|
16
16
|
use std::ptr::null_mut;
|
|
17
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
17
|
+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
18
18
|
|
|
19
19
|
// rusty_v8 doesn't wrap the runtime `v8::Isolate::SetStackLimit(uintptr_t)`, so
|
|
20
20
|
// link the public V8 symbol directly (stable across V8 versions). It sets the
|
|
@@ -24,7 +24,7 @@ unsafe extern "C" {
|
|
|
24
24
|
fn v8__Isolate__SetStackLimit(isolate: *mut c_void, stack_limit: usize);
|
|
25
25
|
// V8's own (exported) accessors down to the conservative-GC-scan Stack
|
|
26
26
|
// object, so we can re-point its stack_start per op when V8 runs on a Ruby
|
|
27
|
-
// Fiber (see
|
|
27
|
+
// Fiber (see StackScope / discover_scan_start_field). Member fns:
|
|
28
28
|
// the first arg is `this`. The public v8::Isolate* IS i::Isolate*.
|
|
29
29
|
#[link_name = "_ZN2v88internal7Isolate4heapEv"]
|
|
30
30
|
fn v8__internal__Isolate__heap(isolate: *mut c_void) -> *mut c_void;
|
|
@@ -52,7 +52,7 @@ pub(crate) fn current_real_isolate() -> *mut c_void {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// Locate V8's conservative-GC-scan stack_start field
|
|
55
|
-
// (heap::base::Stack::current_segment_.start) so
|
|
55
|
+
// (heap::base::Stack::current_segment_.start) so StackScope can
|
|
56
56
|
// re-point it per op. The scanner walks [SP, stack_start); on a Ruby Fiber V8's
|
|
57
57
|
// stack_start is still the NATIVE thread top, a different region, so the walk
|
|
58
58
|
// runs off the fiber's mapped top into the guard page and SEGVs (the residual
|
|
@@ -178,6 +178,13 @@ fn current_region_bounds_cached(addr: usize) -> (usize, usize) {
|
|
|
178
178
|
// The [start, end) of the /proc/self/maps mapping containing `addr`. Linux only;
|
|
179
179
|
// (0, 0) elsewhere (and the caller falls back). Reads the file fresh — slow, so
|
|
180
180
|
// only called on a cache miss (a new fiber).
|
|
181
|
+
//
|
|
182
|
+
// Everything that needs a FIBER's real bounds degrades on the (0, 0) platforms,
|
|
183
|
+
// macOS being the shipped one: the stack limit drops to a fixed budget below the
|
|
184
|
+
// SP (see StackScope::enter), and a nested op can no longer widen its GC-scan
|
|
185
|
+
// start to the enclosing op's, so it narrows to its own frame. Both are the
|
|
186
|
+
// pre-fiber-support behaviour rather than a crash, but a darwin implementation
|
|
187
|
+
// (mach_vm_region on the current task) would close the gap.
|
|
181
188
|
#[cfg(target_os = "linux")]
|
|
182
189
|
fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
183
190
|
use std::io::Read;
|
|
@@ -216,11 +223,17 @@ fn query_region_bounds(_addr: usize) -> (usize, usize) {
|
|
|
216
223
|
// Set from RUSTY_RACER_STACK_DEBUG at init; gates the per-op stack diagnostics.
|
|
217
224
|
pub(crate) static STACK_DEBUG: AtomicBool = AtomicBool::new(false);
|
|
218
225
|
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
226
|
+
// How V8 describes the stack an op runs on: the stack limit, and — on a Ruby
|
|
227
|
+
// Fiber — the conservative-GC-scan start. Both live in the ISOLATE, not in the
|
|
228
|
+
// op, so an op that runs on a DIFFERENT stack than the one enclosing it (a host
|
|
229
|
+
// callback that resumes a Fiber which evals again) has to install its own and
|
|
230
|
+
// put the enclosing op's back on the way out. Hence a scope: after enter() V8
|
|
231
|
+
// describes the stack we are on now, after the drop the one it described before.
|
|
232
|
+
//
|
|
233
|
+
// In-thread V8 runs wherever the Ruby code is: usually the native thread's
|
|
234
|
+
// pthread stack, but also a Ruby Fiber's separate mmap'd stack (Capybara::Result
|
|
235
|
+
// is an Enumerator) that pthread can't see. The limit MUST sit between the
|
|
236
|
+
// current SP and the real bottom of whatever stack we're on:
|
|
224
237
|
// * Too high (above SP) and V8 declares a FALSE overflow on entry.
|
|
225
238
|
// * Too low (below the real bottom) and a deep recursion grows past the
|
|
226
239
|
// mapped stack and SEGVs the unmapped guard page below it.
|
|
@@ -228,85 +241,229 @@ pub(crate) static STACK_DEBUG: AtomicBool = AtomicBool::new(false);
|
|
|
228
241
|
// native stack, anchor to its pthread bottom; on a fiber, find the bottom of the
|
|
229
242
|
// /proc/self/maps region holding the SP (the fiber's real bottom — anchoring to
|
|
230
243
|
// SP minus a fixed guard punched through the bottom of Avo's small/deep Capybara
|
|
231
|
-
// fibers and SEGV'd).
|
|
232
|
-
//
|
|
244
|
+
// fibers and SEGV'd).
|
|
245
|
+
//
|
|
246
|
+
// The limit does double duty, which is why a STALE one is fatal rather than
|
|
247
|
+
// merely wrong: v8::Isolate::SetStackLimit also sets stack_size_ to
|
|
248
|
+
// (base::Stack::GetStackStart() - limit), and V8's "am I on the central stack?"
|
|
249
|
+
// test is (native_top - stack_size_ - margin) < addr <= native_top — that is,
|
|
250
|
+
// exactly (limit - margin, native_top]. Throwing (and GC) CHECKs that window and
|
|
251
|
+
// V8_Fatals the process on a miss. So an op running on a fiber under the
|
|
252
|
+
// ENCLOSING op's native-stack limit doesn't merely false-overflow: the
|
|
253
|
+
// RangeError it then tries to throw aborts. Installing the limit for the stack
|
|
254
|
+
// we are actually on fixes both at once.
|
|
233
255
|
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
256
|
+
// RESIDUAL (GC scan across a stack SWITCH): V8 tracks ONE scan segment per
|
|
257
|
+
// isolate, so while a nested op runs on a fiber its enclosing op's frames on the
|
|
258
|
+
// NATIVE stack aren't conservatively scanned — the two live in different
|
|
259
|
+
// mappings and only one range can be described. (A nested op that did NOT switch
|
|
260
|
+
// stacks is fine: it keeps the enclosing op's start, which covers both.) Handles
|
|
261
|
+
// are unaffected — this
|
|
262
|
+
// build has v8_enable_direct_handle off, so every Local lives in a HandleScope
|
|
263
|
+
// block and is iterated precisely, and JS frames are walked precisely from the
|
|
264
|
+
// frame pointers whichever stack they sit on. Only a raw Tagged<> in V8's own
|
|
265
|
+
// C++ frames would be missed, and V8 keeps those in handles across anything that
|
|
266
|
+
// can allocate. There's no fix available: Stack has no API to register a second
|
|
267
|
+
// segment for the same thread.
|
|
240
268
|
//
|
|
241
|
-
// LIMITATION (worker-thread fibers): the
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
// ABOVE that top
|
|
246
|
-
// below later fiber mmaps
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
pub(crate)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// V8
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
269
|
+
// LIMITATION (worker-thread fibers): only the window's LOWER bound follows the
|
|
270
|
+
// limit. Its upper bound is base::Stack::GetStackStart(), the native pthread
|
|
271
|
+
// top, cached per thread with no way to retarget it (on POSIX
|
|
272
|
+
// base::Stack::SetCurrentThreadStackBounds is UNREACHABLE()). A fiber mmap'd
|
|
273
|
+
// ABOVE that top — the common case on a NON-main native thread, whose stack sits
|
|
274
|
+
// below later fiber mmaps — is outside the window whatever limit we set, so V8
|
|
275
|
+
// aborts on the next throw or GC. On the main thread the process stack is the
|
|
276
|
+
// highest address, so every fiber is below it and the window covers it — the
|
|
277
|
+
// Capybara/Avo case. See README.
|
|
278
|
+
pub(crate) struct StackScope<'a> {
|
|
279
|
+
real_isolate: *mut c_void,
|
|
280
|
+
// Address of V8's conservative-GC-scan start field, or 0 when enter()
|
|
281
|
+
// installed nothing (discovery failed, or no sane limit) — the drop then
|
|
282
|
+
// restores nothing.
|
|
283
|
+
scan_start_field: usize,
|
|
284
|
+
// The limit V8 currently holds for this isolate. Tracked by the caller
|
|
285
|
+
// because V8 exposes no getter, and a nested op needs the enclosing op's
|
|
286
|
+
// value to put back. 0 before the isolate's first op.
|
|
287
|
+
installed_limit: &'a AtomicUsize,
|
|
288
|
+
// What the drop has to put back, or 0 for "nothing changed, nothing to undo".
|
|
289
|
+
// Most re-entrant ops run in deeper frames of the SAME stack and so compute
|
|
290
|
+
// the very values already installed; skipping those writes keeps a nested op
|
|
291
|
+
// as cheap as it was before it started describing its own stack.
|
|
292
|
+
restore_limit: usize,
|
|
293
|
+
restore_scan_start: usize,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
impl<'a> StackScope<'a> {
|
|
297
|
+
// Describe the stack THIS call runs on to the isolate, until the drop. Must
|
|
298
|
+
// be called with the isolate ENTERED: Isolate::Enter sets the scan start to
|
|
299
|
+
// the native top, so entering afterwards would clobber a fiber override.
|
|
300
|
+
//
|
|
301
|
+
// `enclosing_scan_start` is the scan start of the op this one is nested in,
|
|
302
|
+
// or None at the outermost op, where no enclosing op exists — then the drop
|
|
303
|
+
// restores nothing (the values left behind belong to no live frame, and the
|
|
304
|
+
// next op installs its own before any JS runs). Because Enter re-points the
|
|
305
|
+
// field, it must be read BEFORE entering. It doubles as the value the drop
|
|
306
|
+
// puts back and, when a nested op didn't switch stacks, as this op's own
|
|
307
|
+
// start (see below). `real_isolate` is the raw v8::Isolate* read out of
|
|
308
|
+
// iso_ptr; `stack_top` is a live address the caller captured ABOVE every V8
|
|
309
|
+
// frame of this op (used only on a fiber).
|
|
310
|
+
pub(crate) fn enter(
|
|
311
|
+
real_isolate: *mut c_void,
|
|
312
|
+
scan_start_field: usize,
|
|
313
|
+
enclosing_scan_start: Option<usize>,
|
|
314
|
+
installed_limit: &'a AtomicUsize,
|
|
315
|
+
stack_top: usize,
|
|
316
|
+
) -> Self {
|
|
317
|
+
// Only a nested op has an enclosing description to preserve and restore.
|
|
318
|
+
let nested = enclosing_scan_start.is_some();
|
|
319
|
+
// 0 doubles as "no enclosing value" throughout: it is never a real start.
|
|
320
|
+
let prev_scan_start = enclosing_scan_start.unwrap_or(0);
|
|
321
|
+
let sp_marker = 0u8;
|
|
322
|
+
let sp = &sp_marker as *const u8 as usize;
|
|
323
|
+
let (nbottom, ntop) = native_stack_bounds_cached();
|
|
324
|
+
let on_native = nbottom != 0 && sp > nbottom && sp <= ntop;
|
|
325
|
+
// Reserve below the limit for V8's own RangeError-throw frames.
|
|
326
|
+
const NATIVE_GUARD: usize = 128 * 1024;
|
|
327
|
+
// V8 throws when SP descends to the limit, then needs some real stack
|
|
328
|
+
// BELOW it to build the RangeError (and V8 itself allows growing a little
|
|
329
|
+
// past the limit — its overflow slack). On a fiber that reserve must NOT
|
|
330
|
+
// cross the fiber's real bottom (the mapping below it is an unmapped
|
|
331
|
+
// guard -> SEGV), so keep it comfortably above V8's slack.
|
|
332
|
+
const FIBER_RESERVE: usize = 64 * 1024;
|
|
333
|
+
let mut region = (0usize, 0usize);
|
|
334
|
+
let limit = if on_native {
|
|
335
|
+
nbottom + NATIVE_GUARD
|
|
276
336
|
} else {
|
|
277
|
-
|
|
337
|
+
// Anchor to the FIBER's real bottom (the /proc/self/maps region
|
|
338
|
+
// holding the SP), not the SP: SP - fixed_guard can punch through the
|
|
339
|
+
// bottom of a small/deep fiber stack and SEGV (Avo's deep Capybara
|
|
340
|
+
// filter chain). Reserve FIBER_RESERVE above the bottom for the
|
|
341
|
+
// throw, but keep the limit below the SP so we don't false-overflow;
|
|
342
|
+
// on a nearly-full fiber that clamps the headroom down (an early but
|
|
343
|
+
// CLEAN RangeError).
|
|
344
|
+
region = current_region_bounds_cached(sp);
|
|
345
|
+
if region.0 != 0 {
|
|
346
|
+
(region.0 + FIBER_RESERVE).min(sp.saturating_sub(8 * 1024))
|
|
347
|
+
} else {
|
|
348
|
+
// Region unknown — only Linux can look a fiber's bounds up (see
|
|
349
|
+
// query_region_bounds), so this is the macOS path. Best effort:
|
|
350
|
+
// hand JS a fixed budget below the SP and hope the fiber is that
|
|
351
|
+
// deep. It usually is for an OUTERMOST fiber op, whose SP sits
|
|
352
|
+
// near the fiber's top, and much less reliably for a NESTED one,
|
|
353
|
+
// whose SP is already far down the fiber — there this can land
|
|
354
|
+
// below the fiber's mapped bottom, and V8 then grows through the
|
|
355
|
+
// guard page (SEGV) instead of throwing. Fixing that properly
|
|
356
|
+
// means a mach_vm_region lookup for darwin.
|
|
357
|
+
sp.saturating_sub(64 * 1024)
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
// Opt-in diagnostics (RUSTY_RACER_STACK_DEBUG): the SP vs the native
|
|
361
|
+
// stack [nbottom, ntop], the fiber region (if any), the per-op limit, and
|
|
362
|
+
// whether the SP is above the limit. A crash with sp_above_limit=false
|
|
363
|
+
// means the limit is wrong for the current stack.
|
|
364
|
+
if STACK_DEBUG.load(Ordering::Relaxed) {
|
|
365
|
+
eprintln!(
|
|
366
|
+
"[rusty stack] sp={sp:#x} nbottom={nbottom:#x} ntop={ntop:#x} \
|
|
367
|
+
region=[{:#x},{:#x}) limit={limit:#x} fiber={} sp_above_limit={} \
|
|
368
|
+
fiber_above_native={}",
|
|
369
|
+
region.0,
|
|
370
|
+
region.1,
|
|
371
|
+
!on_native,
|
|
372
|
+
sp > limit,
|
|
373
|
+
!on_native && nbottom != 0 && sp > ntop,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if limit == 0 {
|
|
377
|
+
// Couldn't determine a sane limit — leave V8's default in place, and
|
|
378
|
+
// with it a scope that restores nothing.
|
|
379
|
+
return Self {
|
|
380
|
+
real_isolate,
|
|
381
|
+
scan_start_field: 0,
|
|
382
|
+
installed_limit,
|
|
383
|
+
restore_limit: 0,
|
|
384
|
+
restore_scan_start: 0,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
// installed_limit is the sole record of what V8 holds (it has no getter),
|
|
388
|
+
// so an equal value means the isolate is already described correctly and
|
|
389
|
+
// the call can be skipped. Nothing else moves the limit behind our back:
|
|
390
|
+
// Isolate::Enter/Exit leave the stack guard alone, and V8 re-points it
|
|
391
|
+
// itself only under v8::Locker thread archiving (rusty_v8 exposes no
|
|
392
|
+
// Locker, and an isolate here is thread-confined) or wasm stack
|
|
393
|
+
// switching (no wasm stacks exist in this embedding).
|
|
394
|
+
let prev_limit = installed_limit.load(Ordering::Relaxed);
|
|
395
|
+
let changed_limit = limit != prev_limit;
|
|
396
|
+
if changed_limit {
|
|
397
|
+
unsafe { v8__Isolate__SetStackLimit(real_isolate, limit) };
|
|
398
|
+
installed_limit.store(limit, Ordering::Relaxed);
|
|
399
|
+
}
|
|
400
|
+
// Only a nested op owes anything back: at the outermost op prev_limit is
|
|
401
|
+
// the previous, already-finished op's, which describes no live frame.
|
|
402
|
+
let restore_limit = if nested && changed_limit { prev_limit } else { 0 };
|
|
403
|
+
// Point V8's conservative-GC-scan start at the stack we're on. The
|
|
404
|
+
// scanner walks [marker, start), so on a fiber a native start runs it off
|
|
405
|
+
// the fiber's mapped top into unmapped memory and SEGVs (Avo's Capybara
|
|
406
|
+
// filter chain). Always take the WIDEST start that is still on this
|
|
407
|
+
// stack, because everything between the marker and it gets scanned and an
|
|
408
|
+
// enclosing op's frames sit above ours:
|
|
409
|
+
// * native stack — the native top, i.e. what V8 itself uses.
|
|
410
|
+
// * a fiber we ENTERED (the enclosing op is elsewhere) — this op's
|
|
411
|
+
// `stack_top`, which keeps the range between two real stack pointers
|
|
412
|
+
// (marker..stack_top) and so guaranteed mapped. We can't use the
|
|
413
|
+
// /proc/maps region top: that mapping isn't reliably contiguous, so
|
|
414
|
+
// the scan could still hit a hole below it.
|
|
415
|
+
// * a fiber we were ALREADY on (a nested op under a host callback that
|
|
416
|
+
// didn't switch stacks) — the enclosing op's start, which is above
|
|
417
|
+
// ours and in the same mapping, so it covers both ops' frames.
|
|
418
|
+
let new_scan_start = if on_native {
|
|
419
|
+
unsafe { v8__base__Stack__GetStackStart() }
|
|
420
|
+
} else if nested && region.0 != 0 && prev_scan_start > sp && prev_scan_start < region.1 {
|
|
421
|
+
prev_scan_start
|
|
422
|
+
} else {
|
|
423
|
+
stack_top
|
|
424
|
+
};
|
|
425
|
+
// Install it, and work out what the drop owes the enclosing op. The value
|
|
426
|
+
// to put back is the caller's PRE-ENTER snapshot, not whatever the field
|
|
427
|
+
// holds now: Isolate::Enter re-points the start at the native top, so on
|
|
428
|
+
// the foreign-isolate reentry path the field has already been clobbered
|
|
429
|
+
// by the time we get here, and restoring that would strand an enclosing
|
|
430
|
+
// fiber op with a start on the wrong stack.
|
|
431
|
+
let restore_scan_start = if scan_start_field != 0 && new_scan_start != 0 {
|
|
432
|
+
let field = scan_start_field as *mut usize;
|
|
433
|
+
if unsafe { field.read() } != new_scan_start {
|
|
434
|
+
unsafe { field.write(new_scan_start) };
|
|
435
|
+
}
|
|
436
|
+
if nested && prev_scan_start != 0 && prev_scan_start != new_scan_start {
|
|
437
|
+
prev_scan_start
|
|
438
|
+
} else {
|
|
439
|
+
0 // nothing enclosing to put back, or the drop would be a no-op
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
0 // nothing installed -> nothing to undo
|
|
443
|
+
};
|
|
444
|
+
Self {
|
|
445
|
+
real_isolate,
|
|
446
|
+
scan_start_field,
|
|
447
|
+
installed_limit,
|
|
448
|
+
restore_limit,
|
|
449
|
+
restore_scan_start,
|
|
278
450
|
}
|
|
279
|
-
};
|
|
280
|
-
if limit == 0 {
|
|
281
|
-
return; // couldn't determine a sane limit — leave V8's default
|
|
282
|
-
}
|
|
283
|
-
unsafe { v8__Isolate__SetStackLimit(real_isolate, limit) };
|
|
284
|
-
// On a fiber, re-point V8's conservative-GC-scan stack_start to `stack_top`
|
|
285
|
-
// — a live address captured by the caller ABOVE every V8 frame of this op.
|
|
286
|
-
// Enter() set the start to the NATIVE top (a different region); the scanner
|
|
287
|
-
// walks [marker, start), so a native start runs it off the fiber's mapped
|
|
288
|
-
// top into unmapped memory and SEGVs. Anchoring to stack_top keeps the whole
|
|
289
|
-
// scan range between two real stack pointers (marker..stack_top), so it's
|
|
290
|
-
// guaranteed mapped, and every V8 root (all below stack_top) is still found.
|
|
291
|
-
// (We can't use the /proc/maps region top here: that mapping isn't reliably
|
|
292
|
-
// contiguous, so the scan could still hit a hole below it.)
|
|
293
|
-
if !on_native && stack_top != 0 && scan_start_field != 0 {
|
|
294
|
-
unsafe { (scan_start_field as *mut usize).write(stack_top) };
|
|
295
451
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
impl Drop for StackScope<'_> {
|
|
455
|
+
// Put the enclosing op's description back, in the reverse order enter()
|
|
456
|
+
// installed it. A zero means there is nothing to put back: enter() changed
|
|
457
|
+
// nothing, or this was the isolate's first op and there is no earlier
|
|
458
|
+
// description. Leaving the last op's settings behind is harmless — every op
|
|
459
|
+
// installs its own before any JS runs.
|
|
460
|
+
fn drop(&mut self) {
|
|
461
|
+
if self.scan_start_field != 0 && self.restore_scan_start != 0 {
|
|
462
|
+
unsafe { (self.scan_start_field as *mut usize).write(self.restore_scan_start) };
|
|
463
|
+
}
|
|
464
|
+
if self.restore_limit != 0 {
|
|
465
|
+
unsafe { v8__Isolate__SetStackLimit(self.real_isolate, self.restore_limit) };
|
|
466
|
+
self.installed_limit.store(self.restore_limit, Ordering::Relaxed);
|
|
467
|
+
}
|
|
311
468
|
}
|
|
312
469
|
}
|
data/lib/rusty_racer/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rusty_racer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keita Urashima
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|