Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion crates/pctx_executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mutex<()>> = std::sync::LazyLock::new(|| {
init_v8_platform();
Mutex::new(())
});

pub type Result<T> = std::result::Result<T, DenoExecutorError>;

#[derive(Clone, Default)]
Expand Down Expand Up @@ -126,6 +140,11 @@ pub async fn execute(code: &str, options: ExecuteOptions) -> Result<ExecuteResul
code_length = code.len(),
"Code submitted for typecheck & execution"
);

// Acquire V8 mutex for the entire operation (type check + execution)
// This ensures no concurrent V8 isolate usage across the process
let _guard = V8_MUTEX.lock().await;

let check_result = run_type_check(code).await?;

// Check if we have diagnostics
Expand Down Expand Up @@ -270,6 +289,8 @@ async fn execute_code(
) -> anyhow::Result<InternalExecuteResult> {
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) => {
Expand Down
40 changes: 40 additions & 0 deletions crates/pctx_executor/src/tests/concurrent_v8_stress.rs
Original file line number Diff line number Diff line change
@@ -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();
}
}
1 change: 1 addition & 0 deletions crates/pctx_executor/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 19 additions & 18 deletions crates/pctx_type_check_runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Mutex<()>> = 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
///
Expand Down Expand Up @@ -200,17 +203,15 @@ pub async fn type_check(code: &str) -> Result<CheckResult> {
});
}

// 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 =
Expand Down