diff --git a/CHANGELOG.md b/CHANGELOG.md index 73d9c58..e976267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- JS runtime race condition by moving the V8 mutex to be held for the entire typecheck/execute process. This previously caused a panic: `../../../../third_party/libc++/src/include/__vector/vector.h:416: libc++ Hardening assertion __n < size() failed: vector[] index out of bounds` + ## [v0.4.3] - 2026-01-27 ### Added diff --git a/crates/pctx_executor/src/lib.rs b/crates/pctx_executor/src/lib.rs index a470b5f..e46b53e 100644 --- a/crates/pctx_executor/src/lib.rs +++ b/crates/pctx_executor/src/lib.rs @@ -3,13 +3,27 @@ use deno_core::ModuleCodeString; use deno_core::RuntimeOptions; use deno_core::anyhow; use deno_core::error::CoreError; +use futures::lock::Mutex; use pctx_code_execution_runtime::CallbackRegistry; -pub use pctx_type_check_runtime::{CheckResult, Diagnostic, is_relevant_error, type_check}; +pub use pctx_type_check_runtime::{CheckResult, Diagnostic, is_relevant_error}; +use pctx_type_check_runtime::{init_v8_platform, type_check}; use serde::{Deserialize, Serialize}; use std::rc::Rc; use thiserror::Error; use tracing::{debug, warn}; +/// Process-wide mutex to serialize all V8 isolate creation and usage. +/// +/// V8 isolates share platform-level state (code pages, thread pool, etc.) that is not +/// safe to access concurrently from multiple OS threads. All code that creates or uses +/// a `JsRuntime` must hold this lock for the runtime's entire lifetime. +/// +/// This mutex is acquired by `execute()` and held for both type checking and code execution. +static V8_MUTEX: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + init_v8_platform(); + Mutex::new(()) +}); + pub type Result = std::result::Result; #[derive(Clone, Default)] @@ -126,6 +140,11 @@ pub async fn execute(code: &str, options: ExecuteOptions) -> Result anyhow::Result { debug!("Starting code execution"); + // Note: V8_MUTEX is held by the caller (execute()) for the entire operation + // Transpile TypeScript to JavaScript let js_code = match pctx_deno_transpiler::transpile(code, None) { Ok(js) => { diff --git a/crates/pctx_executor/src/tests/concurrent_v8_stress.rs b/crates/pctx_executor/src/tests/concurrent_v8_stress.rs new file mode 100644 index 0000000..e1ba098 --- /dev/null +++ b/crates/pctx_executor/src/tests/concurrent_v8_stress.rs @@ -0,0 +1,40 @@ +//! Stress test for concurrent V8 isolate usage. +//! +//! Deliberately does NOT use #[serial] — the goal is to reproduce the +//! V8 vector-out-of-bounds crash that occurs when type_check and +//! execute_code create JsRuntimes on different OS threads simultaneously. + +use crate::{ExecuteOptions, execute}; + +#[test] +fn test_concurrent_execute_stress() { + // Mirror the production pattern: multiple OS threads each with their own + // single-threaded tokio runtime, calling execute() (type_check + execute_code) + // concurrently. This creates overlapping V8 isolates without serialization. + let handles: Vec<_> = (0..4) + .map(|i| { + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(async { + for j in 0..3 { + let code = + format!("const x{i}_{j}: number = {i} + {j}; export default x{i}_{j};"); + let result = execute(&code, ExecuteOptions::new()).await.unwrap(); + assert!( + result.success, + "iteration {i}_{j} failed: {:?}", + result.diagnostics + ); + } + }) + }) + }) + .collect(); + + for h in handles { + h.join().unwrap(); + } +} diff --git a/crates/pctx_executor/src/tests/mod.rs b/crates/pctx_executor/src/tests/mod.rs index 03a155b..9c7e34c 100644 --- a/crates/pctx_executor/src/tests/mod.rs +++ b/crates/pctx_executor/src/tests/mod.rs @@ -15,6 +15,7 @@ pub(crate) fn init_rustls_crypto() { } mod callback_usage; +mod concurrent_v8_stress; mod default_export_capture; mod diagnostic_filtering; mod mcp_client_usage; diff --git a/crates/pctx_type_check_runtime/src/lib.rs b/crates/pctx_type_check_runtime/src/lib.rs index 245dcbd..442c10a 100644 --- a/crates/pctx_type_check_runtime/src/lib.rs +++ b/crates/pctx_type_check_runtime/src/lib.rs @@ -71,7 +71,6 @@ pub mod ignored_codes; use deno_core::JsRuntime; use deno_core::RuntimeOptions; -use futures::lock::Mutex; use serde::{Deserialize, Serialize}; use std::rc::Rc; use thiserror::Error; @@ -133,12 +132,16 @@ deno_core::extension!( esm = [ dir "src", "type_check_runtime_generated.js" ], ); -// Global mutex to serialize type checking operations and prevent V8 race conditions -static TYPE_CHECK_MUTEX: std::sync::LazyLock> = std::sync::LazyLock::new(|| { - // Initialize V8 platform once - deno_core::JsRuntime::init_platform(None); - Mutex::new(()) -}); +/// Initialize the V8 platform. Must be called before any JsRuntime is created. +/// Safe to call multiple times - only the first call has effect. +static V8_INIT: std::sync::Once = std::sync::Once::new(); + +/// Ensure V8 platform is initialized. Called automatically by type_check. +pub fn init_v8_platform() { + V8_INIT.call_once(|| { + deno_core::JsRuntime::init_platform(None); + }); +} /// Type check TypeScript code using an isolated Deno runtime with TypeScript compiler /// @@ -200,17 +203,15 @@ pub async fn type_check(code: &str) -> Result { }); } - // Create an isolated runtime with the type check snapshot - // Serialize runtime creation to prevent V8 race conditions - let mut js_runtime = { - let _guard = TYPE_CHECK_MUTEX.lock().await; - JsRuntime::new(RuntimeOptions { - module_loader: Some(Rc::new(deno_core::FsModuleLoader)), - startup_snapshot: Some(TYPE_CHECK_SNAPSHOT), - extensions: vec![pctx_type_check_snapshot::init()], - ..Default::default() - }) - }; + // Ensure V8 platform is initialized (safe to call multiple times) + init_v8_platform(); + + let mut js_runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(Rc::new(deno_core::FsModuleLoader)), + startup_snapshot: Some(TYPE_CHECK_SNAPSHOT), + extensions: vec![pctx_type_check_snapshot::init()], + ..Default::default() + }); // Call the type checking function from the runtime let code_json =