Skip to content

Commit a5970d2

Browse files
committed
feat: add V8 sandbox mode support
- Add v8_helpers module with create_array_buffer_from_vec() helper that handles sandbox vs non-sandbox memory allocation differences - Make CustomAllocator conditional (not available in sandbox mode) - Update all backing store usages to use the new helper - Skip memory limit tests in sandbox mode (V8 manages memory) - Add "sandbox" feature flag (default enabled)
1 parent 27a77e9 commit a5970d2

12 files changed

Lines changed: 143 additions & 70 deletions

Cargo.lock

Lines changed: 6 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ members = ["gc-derive"]
33

44
[package]
55
name = "openworkers-runtime-v8"
6-
version = "0.11.1"
6+
version = "0.11.2"
77
edition = "2024"
88
license = "MIT"
99

10+
[features]
11+
default = []
12+
ptrcomp = ["v8/v8_enable_pointer_compression"]
13+
sandbox = ["v8/v8_enable_sandbox", "serde_v8/v8_enable_sandbox"]
14+
1015
[dependencies]
1116

1217
# GC derive macro (internal)
@@ -16,9 +21,7 @@ gc-derive = { package = "openworkers-runtime-v8-gc-derive", path = "gc-derive" }
1621
openworkers-core = { git = "https://github.com/openworkers/openworkers-core", tag = "v0.11.0" }
1722

1823
# V8 JavaScript engine (fork with UnenteredIsolate + Locker support for pooling)
19-
# Local dev: v8 = { path = "../rusty_v8", features = ["v8_enable_pointer_compression"] }
20-
# After PR merge: v8 = { version = "...", features = ["v8_enable_pointer_compression"] }
21-
v8 = { package = "openworkers-v8", version = "142.2.3", features = ["v8_enable_pointer_compression"] }
24+
v8 = { package = "openworkers-v8", version = "142.2.4" }
2225

2326
# Async runtime
2427
tokio = { version = "1.49.0", features = ["full"] }
@@ -33,7 +36,7 @@ serde = { version = "1.0", features = ["derive"] }
3336
serde_json = "1.0"
3437

3538
# V8 serialization (for automatic Rust <-> JS type conversion)
36-
serde_v8 = { package = "openworkers-serde-v8", version = "0.3.0" }
39+
serde_v8 = { package = "openworkers-serde-v8", version = "0.4.0" }
3740

3841
# V8 Glue - binding macros
3942
glue_v8 = { package = "openworkers-glue-v8", version = "0.5.0" }

src/execution_helpers.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,7 @@ pub fn trigger_fetch_handler(
281281
// Buffered body - pass as Uint8Array for binary support
282282
let len = body_bytes.len();
283283
let vec: Vec<u8> = body_bytes.into(); // zero-copy if uniquely owned
284-
let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(vec).make_shared();
285-
let array_buffer = v8::ArrayBuffer::with_backing_store(scope, &backing_store);
284+
let array_buffer = crate::v8_helpers::create_array_buffer_from_vec(scope, vec);
286285
let uint8_array = v8::Uint8Array::new(scope, array_buffer, 0, len).unwrap();
287286

288287
let body_key = v8::String::new(scope, "body").unwrap();

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub mod security;
4747
pub mod shared_isolate;
4848
pub mod snapshot;
4949
pub mod thread_pinned_pool;
50+
pub mod v8_helpers;
5051
pub mod worker;
5152
pub mod worker_future;
5253

src/locker_managed_isolate.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use std::sync::atomic::AtomicBool;
1111
use v8;
1212

1313
use crate::gc::DeferredDestructionQueue;
14-
use crate::security::{CustomAllocator, HeapLimitState, install_heap_limit_callback};
14+
#[cfg(not(feature = "sandbox"))]
15+
use crate::security::CustomAllocator;
16+
use crate::security::{HeapLimitState, install_heap_limit_callback};
1517
use openworkers_core::RuntimeLimits;
1618

1719
/// A reusable V8 isolate that requires explicit locking via v8::Locker
@@ -52,18 +54,30 @@ impl LockerManagedIsolate {
5254
let heap_initial = limits.heap_initial_mb * 1024 * 1024;
5355
let heap_max = limits.heap_max_mb * 1024 * 1024;
5456

55-
// Create custom ArrayBuffer allocator to enforce memory limits on external memory
56-
// This is critical: V8 heap limits don't cover ArrayBuffers, Uint8Array, etc.
57-
let array_buffer_allocator = CustomAllocator::new(heap_max, Arc::clone(&memory_limit_hit));
58-
5957
// Load snapshot (centralized, handles empty file case)
6058
let snapshot_ref = crate::platform::get_snapshot();
6159

6260
// Create isolate with UnenteredIsolate (no auto-enter!)
63-
let mut params = v8::CreateParams::default()
61+
// In sandbox mode, V8 manages memory allocation, so we use the default allocator.
62+
// In non-sandbox mode, we use a custom allocator to enforce memory limits.
63+
#[cfg(not(feature = "sandbox"))]
64+
let params = {
65+
// Create custom ArrayBuffer allocator to enforce memory limits on external memory
66+
// This is critical: V8 heap limits don't cover ArrayBuffers, Uint8Array, etc.
67+
let array_buffer_allocator =
68+
CustomAllocator::new(heap_max, Arc::clone(&memory_limit_hit));
69+
v8::CreateParams::default()
70+
.heap_limits(heap_initial, heap_max)
71+
.array_buffer_allocator(array_buffer_allocator.into_v8_allocator())
72+
.allow_atomics_wait(false)
73+
};
74+
75+
#[cfg(feature = "sandbox")]
76+
let params = v8::CreateParams::default()
6477
.heap_limits(heap_initial, heap_max)
65-
.array_buffer_allocator(array_buffer_allocator.into_v8_allocator())
66-
.allow_atomics_wait(false); // Security: prevent Atomics.wait() from blocking
78+
.allow_atomics_wait(false);
79+
80+
let mut params = params;
6781

6882
if let Some(snapshot_data) = snapshot_ref {
6983
params = params.snapshot_blob((*snapshot_data).into());

src/runtime/callback_handlers.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,11 @@ pub fn populate_stream_chunk_result(
6161
let done_val = v8::Boolean::new(scope, false);
6262
result_obj.set(scope, done_key.into(), done_val.into());
6363

64-
// Create Uint8Array from bytes using backing store transfer
64+
// Create Uint8Array from bytes
6565
// Use Vec::from(bytes) instead of to_vec() - avoids copy if Bytes is uniquely owned
6666
let vec: Vec<u8> = bytes.into();
6767
let len = vec.len();
68-
let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(vec);
69-
let array_buffer =
70-
v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared());
68+
let array_buffer = crate::v8_helpers::create_array_buffer_from_vec(scope, vec);
7169
let uint8_array = v8::Uint8Array::new(scope, array_buffer, 0, len).unwrap();
7270

7371
let value_key = v8::String::new(scope, "value").unwrap();
@@ -107,9 +105,7 @@ pub fn populate_storage_result(
107105

108106
if let Some(body) = maybe_body {
109107
let len = body.len();
110-
let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(body);
111-
let array_buffer =
112-
v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared());
108+
let array_buffer = crate::v8_helpers::create_array_buffer_from_vec(scope, body);
113109
let uint8_array = v8::Uint8Array::new(scope, array_buffer, 0, len).unwrap();
114110
let body_key = v8::String::new(scope, "body").unwrap();
115111
result_obj.set(scope, body_key.into(), uint8_array.into());

src/runtime/crypto.rs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,8 @@ fn setup_digest(scope: &mut v8::PinScope, subtle_obj: v8::Local<v8::Object>) {
141141
let result_bytes = result.as_ref();
142142

143143
// Create ArrayBuffer with result
144-
let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(result_bytes.to_vec());
145144
let array_buffer =
146-
v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared());
145+
crate::v8_helpers::create_array_buffer_from_vec(scope, result_bytes.to_vec());
147146

148147
retval.set(array_buffer.into());
149148
},
@@ -241,9 +240,8 @@ fn setup_hmac(scope: &mut v8::PinScope, subtle_obj: v8::Local<v8::Object>) {
241240
let tag = hmac::sign(&key, &data);
242241
let result_bytes = tag.as_ref();
243242

244-
let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(result_bytes.to_vec());
245243
let array_buffer =
246-
v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared());
244+
crate::v8_helpers::create_array_buffer_from_vec(scope, result_bytes.to_vec());
247245

248246
retval.set(array_buffer.into());
249247
},
@@ -506,18 +504,16 @@ fn setup_ecdsa(scope: &mut v8::PinScope, subtle_obj: v8::Local<v8::Object>) {
506504
let result = v8::Object::new(scope);
507505

508506
// Private key (PKCS#8 format)
509-
let private_backing =
510-
v8::ArrayBuffer::new_backing_store_from_vec(pkcs8_bytes.as_ref().to_vec());
511-
let private_buffer =
512-
v8::ArrayBuffer::with_backing_store(scope, &private_backing.make_shared());
507+
let private_buffer = crate::v8_helpers::create_array_buffer_from_vec(
508+
scope,
509+
pkcs8_bytes.as_ref().to_vec(),
510+
);
513511
let private_key_str = v8::String::new(scope, "privateKey").unwrap();
514512
result.set(scope, private_key_str.into(), private_buffer.into());
515513

516514
// Public key (uncompressed point format)
517-
let public_backing =
518-
v8::ArrayBuffer::new_backing_store_from_vec(public_key_bytes.to_vec());
519515
let public_buffer =
520-
v8::ArrayBuffer::with_backing_store(scope, &public_backing.make_shared());
516+
crate::v8_helpers::create_array_buffer_from_vec(scope, public_key_bytes.to_vec());
521517
let public_key_str = v8::String::new(scope, "publicKey").unwrap();
522518
result.set(scope, public_key_str.into(), public_buffer.into());
523519

@@ -585,9 +581,8 @@ fn setup_ecdsa(scope: &mut v8::PinScope, subtle_obj: v8::Local<v8::Object>) {
585581
}
586582
};
587583

588-
let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(sig.as_ref().to_vec());
589584
let array_buffer =
590-
v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared());
585+
crate::v8_helpers::create_array_buffer_from_vec(scope, sig.as_ref().to_vec());
591586

592587
retval.set(array_buffer.into());
593588
},
@@ -929,9 +924,7 @@ fn setup_rsa(scope: &mut v8::PinScope, subtle_obj: v8::Local<v8::Object>) {
929924

930925
match key_pair.sign(padding, &rng, &data, &mut sig) {
931926
Ok(_) => {
932-
let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(sig);
933-
let array_buffer =
934-
v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared());
927+
let array_buffer = crate::v8_helpers::create_array_buffer_from_vec(scope, sig);
935928
retval.set(array_buffer.into());
936929
}
937930
Err(_) => {

src/runtime/mod.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use std::sync::atomic::AtomicBool;
1515
use tokio::sync::{Notify, mpsc};
1616
use v8;
1717

18-
use crate::security::{CustomAllocator, HeapLimitState, install_heap_limit_callback};
18+
#[cfg(not(feature = "sandbox"))]
19+
use crate::security::CustomAllocator;
20+
use crate::security::{HeapLimitState, install_heap_limit_callback};
1921
use bindings::LogCallback;
2022
use openworkers_core::{DatabaseResult, KvResult, RuntimeLimits, StorageResult, WorkerCode};
2123

@@ -129,18 +131,30 @@ impl Runtime {
129131
let heap_initial = limits.heap_initial_mb * 1024 * 1024;
130132
let heap_max = limits.heap_max_mb * 1024 * 1024;
131133

132-
// Create custom ArrayBuffer allocator to enforce memory limits on external memory
133-
// This is critical: V8 heap limits don't cover ArrayBuffers, Uint8Array, etc.
134-
let array_buffer_allocator = CustomAllocator::new(heap_max, Arc::clone(&memory_limit_hit));
135-
136134
// Load snapshot (centralized, handles empty file case)
137135
let snapshot_ref = crate::platform::get_snapshot();
138136

139-
// Create isolate with custom allocator and heap limits
140-
let mut params = v8::CreateParams::default()
137+
// Create isolate params
138+
// In sandbox mode, V8 manages memory allocation, so we use the default allocator.
139+
// In non-sandbox mode, we use a custom allocator to enforce memory limits.
140+
#[cfg(not(feature = "sandbox"))]
141+
let params = {
142+
// Create custom ArrayBuffer allocator to enforce memory limits on external memory
143+
// This is critical: V8 heap limits don't cover ArrayBuffers, Uint8Array, etc.
144+
let array_buffer_allocator =
145+
CustomAllocator::new(heap_max, Arc::clone(&memory_limit_hit));
146+
v8::CreateParams::default()
147+
.heap_limits(heap_initial, heap_max)
148+
.array_buffer_allocator(array_buffer_allocator.into_v8_allocator())
149+
.allow_atomics_wait(false)
150+
};
151+
152+
#[cfg(feature = "sandbox")]
153+
let params = v8::CreateParams::default()
141154
.heap_limits(heap_initial, heap_max)
142-
.array_buffer_allocator(array_buffer_allocator.into_v8_allocator())
143-
.allow_atomics_wait(false); // Security: prevent Atomics.wait() from blocking
155+
.allow_atomics_wait(false);
156+
157+
let mut params = params;
144158

145159
if let Some(snapshot_data) = snapshot_ref {
146160
params = params.snapshot_blob((*snapshot_data).into());

src/security/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727
//! let _cpu = CpuEnforcer::new(isolate_handle.clone(), 50); // 50ms
2828
//! ```
2929
30+
#[cfg(not(feature = "sandbox"))]
3031
mod array_buffer_allocator;
3132
mod cpu_enforcer;
3233
mod cpu_timer;
3334
mod heap_limit;
3435
mod timeout_guard;
3536

37+
#[cfg(not(feature = "sandbox"))]
3638
pub use array_buffer_allocator::CustomAllocator;
3739
pub use cpu_enforcer::CpuEnforcer;
3840
pub use cpu_timer::{CpuTimer, get_thread_cpu_time};

src/shared_isolate.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::sync::Arc;
44
use std::sync::atomic::AtomicBool;
55
use v8;
66

7+
#[cfg(not(feature = "sandbox"))]
78
use crate::security::CustomAllocator;
89
use openworkers_core::RuntimeLimits;
910

@@ -37,18 +38,30 @@ impl SharedIsolate {
3738
let heap_initial = limits.heap_initial_mb * 1024 * 1024;
3839
let heap_max = limits.heap_max_mb * 1024 * 1024;
3940

40-
// Create custom ArrayBuffer allocator to enforce memory limits on external memory
41-
// This is critical: V8 heap limits don't cover ArrayBuffers, Uint8Array, etc.
42-
let array_buffer_allocator = CustomAllocator::new(heap_max, Arc::clone(&memory_limit_hit));
43-
4441
// Load snapshot (centralized, handles empty file case)
4542
let snapshot_ref = crate::platform::get_snapshot();
4643

47-
// Create isolate with custom allocator and heap limits
48-
let mut params = v8::CreateParams::default()
44+
// Create isolate params
45+
// In sandbox mode, V8 manages memory allocation, so we use the default allocator.
46+
// In non-sandbox mode, we use a custom allocator to enforce memory limits.
47+
#[cfg(not(feature = "sandbox"))]
48+
let params = {
49+
// Create custom ArrayBuffer allocator to enforce memory limits on external memory
50+
// This is critical: V8 heap limits don't cover ArrayBuffers, Uint8Array, etc.
51+
let array_buffer_allocator =
52+
CustomAllocator::new(heap_max, Arc::clone(&memory_limit_hit));
53+
v8::CreateParams::default()
54+
.heap_limits(heap_initial, heap_max)
55+
.array_buffer_allocator(array_buffer_allocator.into_v8_allocator())
56+
.allow_atomics_wait(false)
57+
};
58+
59+
#[cfg(feature = "sandbox")]
60+
let params = v8::CreateParams::default()
4961
.heap_limits(heap_initial, heap_max)
50-
.array_buffer_allocator(array_buffer_allocator.into_v8_allocator())
51-
.allow_atomics_wait(false); // Security: prevent Atomics.wait() from blocking
62+
.allow_atomics_wait(false);
63+
64+
let mut params = params;
5265

5366
if let Some(snapshot_data) = snapshot_ref {
5467
params = params.snapshot_blob((*snapshot_data).into());

0 commit comments

Comments
 (0)