Merge pull request #601 from h4ckf0r0day/fix/module-dedupe-v8-oom
fix(js): prevent duplicate module evaluation and contain V8 heap exhaustion
This commit is contained in:
@@ -82,6 +82,11 @@ pub struct ObscuraModuleLoader {
|
|||||||
standalone_client: Option<Arc<obscura_net::ObscuraHttpClient>>,
|
standalone_client: Option<Arc<obscura_net::ObscuraHttpClient>>,
|
||||||
import_map: Rc<RefCell<ImportMap>>,
|
import_map: Rc<RefCell<ImportMap>>,
|
||||||
activity: Arc<ModuleLoadActivity>,
|
activity: Arc<ModuleLoadActivity>,
|
||||||
|
/// Canonical and requested specifiers fetched into deno_core's module map.
|
||||||
|
/// The runtime uses a cursor into this append-only list to associate a
|
||||||
|
/// prepared root with the dependencies that its successful evaluation also
|
||||||
|
/// evaluates.
|
||||||
|
loaded_specifiers: Rc<RefCell<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObscuraModuleLoader {
|
impl ObscuraModuleLoader {
|
||||||
@@ -110,6 +115,7 @@ impl ObscuraModuleLoader {
|
|||||||
standalone_client: Some(standalone_client),
|
standalone_client: Some(standalone_client),
|
||||||
import_map,
|
import_map,
|
||||||
activity: Arc::new(ModuleLoadActivity::default()),
|
activity: Arc::new(ModuleLoadActivity::default()),
|
||||||
|
loaded_specifiers: Rc::new(RefCell::new(Vec::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,12 +132,17 @@ impl ObscuraModuleLoader {
|
|||||||
standalone_client: None,
|
standalone_client: None,
|
||||||
import_map,
|
import_map,
|
||||||
activity: Arc::new(ModuleLoadActivity::default()),
|
activity: Arc::new(ModuleLoadActivity::default()),
|
||||||
|
loaded_specifiers: Rc::new(RefCell::new(Vec::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn activity(&self) -> Arc<ModuleLoadActivity> {
|
pub(crate) fn activity(&self) -> Arc<ModuleLoadActivity> {
|
||||||
self.activity.clone()
|
self.activity.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn loaded_specifiers(&self) -> Rc<RefCell<Vec<String>>> {
|
||||||
|
self.loaded_specifiers.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn io_err(msg: String) -> ModuleLoaderError {
|
fn io_err(msg: String) -> ModuleLoaderError {
|
||||||
@@ -194,6 +205,8 @@ impl ModuleLoader for ObscuraModuleLoader {
|
|||||||
// plain Option<String> rather than borrowing &self across an `await`.
|
// plain Option<String> rather than borrowing &self across an `await`.
|
||||||
let proxy_url = self.proxy_url.clone();
|
let proxy_url = self.proxy_url.clone();
|
||||||
let activity = self.activity.clone();
|
let activity = self.activity.clone();
|
||||||
|
let loaded_specifiers = self.loaded_specifiers.clone();
|
||||||
|
loaded_specifiers.borrow_mut().push(url.clone());
|
||||||
// Register before returning the future. The lifecycle can inspect the
|
// Register before returning the future. The lifecycle can inspect the
|
||||||
// runtime between deno_core accepting the load and first polling it.
|
// runtime between deno_core accepting the load and first polling it.
|
||||||
// Keeping the guard inside the future makes cancellation/navigation
|
// Keeping the guard inside the future makes cancellation/navigation
|
||||||
@@ -252,6 +265,11 @@ impl ModuleLoader for ObscuraModuleLoader {
|
|||||||
let found = ModuleSpecifier::parse(resp.url.as_str()).map_err(|e| {
|
let found = ModuleSpecifier::parse(resp.url.as_str()).map_err(|e| {
|
||||||
io_err(format!("Invalid final module URL {}: {}", resp.url, e))
|
io_err(format!("Invalid final module URL {}: {}", resp.url, e))
|
||||||
})?;
|
})?;
|
||||||
|
if found.as_str() != requested.as_str() {
|
||||||
|
loaded_specifiers
|
||||||
|
.borrow_mut()
|
||||||
|
.push(found.to_string());
|
||||||
|
}
|
||||||
let code = obscura_net::decode_non_html(&resp.body, resp.content_type());
|
let code = obscura_net::decode_non_html(&resp.body, resp.content_type());
|
||||||
Ok(ModuleSource::new_with_redirect(
|
Ok(ModuleSource::new_with_redirect(
|
||||||
deno_core::ModuleType::JavaScript,
|
deno_core::ModuleType::JavaScript,
|
||||||
|
|||||||
@@ -51,6 +51,41 @@ static SNAPSHOT: &[u8] = include_bytes!(env!("OBSCURA_SNAPSHOT_PATH"));
|
|||||||
static ISOLATE_CREATE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
static ISOLATE_CREATE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||||
|
|
||||||
const DEFAULT_CDP_AWAIT_TIMEOUT_MS: u64 = 30_000;
|
const DEFAULT_CDP_AWAIT_TIMEOUT_MS: u64 = 30_000;
|
||||||
|
const HEAP_LIMIT_RECOVERY_HEADROOM_BYTES: usize = 64 * 1024 * 1024;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct HeapLimitState {
|
||||||
|
tripped: std::sync::atomic::AtomicBool,
|
||||||
|
restore_limit: std::sync::atomic::AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_heap_limit_guard(
|
||||||
|
runtime: &mut JsRuntime,
|
||||||
|
isolate_handle: IsolateHandle,
|
||||||
|
state: std::sync::Arc<HeapLimitState>,
|
||||||
|
) {
|
||||||
|
runtime.add_near_heap_limit_callback(move |current_limit, _initial_limit| {
|
||||||
|
let _ = state.restore_limit.compare_exchange(
|
||||||
|
0,
|
||||||
|
current_limit,
|
||||||
|
std::sync::atomic::Ordering::SeqCst,
|
||||||
|
std::sync::atomic::Ordering::SeqCst,
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.tripped
|
||||||
|
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
isolate_handle.terminate_execution();
|
||||||
|
current_limit.saturating_add(HEAP_LIMIT_RECOVERY_HEADROOM_BYTES)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> &str {
|
||||||
|
payload
|
||||||
|
.downcast_ref::<String>()
|
||||||
|
.map(String::as_str)
|
||||||
|
.or_else(|| payload.downcast_ref::<&'static str>().copied())
|
||||||
|
.unwrap_or("unknown panic")
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
fn with_sync_render_loading_disabled<R>(
|
fn with_sync_render_loading_disabled<R>(
|
||||||
@@ -94,6 +129,22 @@ pub struct ObscuraJsRuntime {
|
|||||||
/// construction. Lets a watchdog be armed from `&self` (the CDP dispatcher
|
/// construction. Lets a watchdog be armed from `&self` (the CDP dispatcher
|
||||||
/// only holds `&Page` on the hot path) and is stable for the isolate's life.
|
/// only holds `&Page` on the hot path) and is stable for the isolate's life.
|
||||||
isolate_handle: IsolateHandle,
|
isolate_handle: IsolateHandle,
|
||||||
|
/// Signals that V8 approached its configured heap limit. The callback
|
||||||
|
/// terminates the current script and temporarily raises the limit just
|
||||||
|
/// enough for V8 to unwind instead of aborting the worker process.
|
||||||
|
heap_limit_state: std::sync::Arc<HeapLimitState>,
|
||||||
|
/// Browser module-map evaluation is idempotent. deno_core 0.350 asserts if
|
||||||
|
/// the same ModuleId is evaluated twice, so retain the first outcome for
|
||||||
|
/// duplicate script tags and roots already seen by Obscura.
|
||||||
|
module_evaluations: HashMap<deno_core::ModuleId, Result<(), String>>,
|
||||||
|
/// Append-only record owned by the module loader. A cursor around each
|
||||||
|
/// graph load identifies the dependency specifiers that become evaluated
|
||||||
|
/// with its root.
|
||||||
|
loaded_module_specifiers: Rc<RefCell<Vec<String>>>,
|
||||||
|
/// Successful graph evaluation also evaluates every dependency. Remember
|
||||||
|
/// those URLs so a dependency later encountered as a top-level script is a
|
||||||
|
/// browser-style no-op instead of a second deno_core `mod_evaluate` call.
|
||||||
|
evaluated_module_specifiers: HashMap<String, Result<(), String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fetched and instantiated module graph whose evaluation is intentionally
|
/// A fetched and instantiated module graph whose evaluation is intentionally
|
||||||
@@ -101,6 +152,8 @@ pub struct ObscuraJsRuntime {
|
|||||||
pub struct PreparedModule {
|
pub struct PreparedModule {
|
||||||
module_id: deno_core::ModuleId,
|
module_id: deno_core::ModuleId,
|
||||||
description: String,
|
description: String,
|
||||||
|
entry_specifier: Option<String>,
|
||||||
|
graph_specifiers: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remaining_deadline_ms(deadline: tokio::time::Instant) -> Option<u64> {
|
fn remaining_deadline_ms(deadline: tokio::time::Instant) -> Option<u64> {
|
||||||
@@ -197,6 +250,10 @@ impl ObscuraJsRuntime {
|
|||||||
/// read. Keeping one sample across the task also lets repeated CSSOM reads
|
/// read. Keeping one sample across the task also lets repeated CSSOM reads
|
||||||
/// share the retained layout on pages with running animations.
|
/// share the retained layout on pages with running animations.
|
||||||
fn begin_javascript_task(&mut self) {
|
fn begin_javascript_task(&mut self) {
|
||||||
|
// Some internal callers intentionally ignore script errors. Recover a
|
||||||
|
// heap-limit termination before any later task enters V8 even when the
|
||||||
|
// caller that triggered it did not need the error value.
|
||||||
|
self.recover_heap_limit();
|
||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
begin_animation_task(&mut self.state.borrow_mut());
|
begin_animation_task(&mut self.state.borrow_mut());
|
||||||
}
|
}
|
||||||
@@ -223,11 +280,12 @@ impl ObscuraJsRuntime {
|
|||||||
import_map.clone(),
|
import_map.clone(),
|
||||||
);
|
);
|
||||||
let module_load_activity = module_loader.activity();
|
let module_load_activity = module_loader.activity();
|
||||||
|
let loaded_module_specifiers = module_loader.loaded_specifiers();
|
||||||
let module_loader = Rc::new(module_loader);
|
let module_loader = Rc::new(module_loader);
|
||||||
|
|
||||||
// Build the isolate under the process-wide creation lock so two
|
// Build the isolate under the process-wide creation lock so two
|
||||||
// connection threads never construct isolates concurrently (#430).
|
// connection threads never construct isolates concurrently (#430).
|
||||||
let (runtime, isolate_handle) = {
|
let (runtime, isolate_handle, heap_limit_state) = {
|
||||||
let _create_guard = ISOLATE_CREATE_LOCK
|
let _create_guard = ISOLATE_CREATE_LOCK
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
@@ -241,6 +299,14 @@ impl ObscuraJsRuntime {
|
|||||||
|
|
||||||
runtime.op_state().borrow_mut().put(state_clone);
|
runtime.op_state().borrow_mut().put(state_clone);
|
||||||
|
|
||||||
|
let isolate_handle = runtime.v8_isolate().thread_safe_handle();
|
||||||
|
let heap_limit_state = std::sync::Arc::new(HeapLimitState::default());
|
||||||
|
install_heap_limit_guard(
|
||||||
|
&mut runtime,
|
||||||
|
isolate_handle.clone(),
|
||||||
|
heap_limit_state.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
runtime
|
runtime
|
||||||
.execute_script(
|
.execute_script(
|
||||||
"<obscura:init>",
|
"<obscura:init>",
|
||||||
@@ -248,8 +314,7 @@ impl ObscuraJsRuntime {
|
|||||||
)
|
)
|
||||||
.expect("init should not fail");
|
.expect("init should not fail");
|
||||||
|
|
||||||
let isolate_handle = runtime.v8_isolate().thread_safe_handle();
|
(runtime, isolate_handle, heap_limit_state)
|
||||||
(runtime, isolate_handle)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ObscuraJsRuntime {
|
ObscuraJsRuntime {
|
||||||
@@ -260,9 +325,61 @@ impl ObscuraJsRuntime {
|
|||||||
import_map,
|
import_map,
|
||||||
module_load_activity,
|
module_load_activity,
|
||||||
isolate_handle,
|
isolate_handle,
|
||||||
|
heap_limit_state,
|
||||||
|
module_evaluations: HashMap::new(),
|
||||||
|
loaded_module_specifiers,
|
||||||
|
evaluated_module_specifiers: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restore the configured V8 heap limit after the emergency headroom has
|
||||||
|
/// allowed a terminated allocation to unwind. The callback is then armed
|
||||||
|
/// again so a second hostile script cannot grow the isolate without bound.
|
||||||
|
fn recover_heap_limit(&mut self) -> bool {
|
||||||
|
if !self
|
||||||
|
.heap_limit_state
|
||||||
|
.tripped
|
||||||
|
.swap(false, std::sync::atomic::Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runtime.v8_isolate().cancel_terminate_execution();
|
||||||
|
let restore_limit = self
|
||||||
|
.heap_limit_state
|
||||||
|
.restore_limit
|
||||||
|
.swap(0, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
self.runtime
|
||||||
|
.remove_near_heap_limit_callback(restore_limit);
|
||||||
|
install_heap_limit_guard(
|
||||||
|
&mut self.runtime,
|
||||||
|
self.isolate_handle.clone(),
|
||||||
|
self.heap_limit_state.clone(),
|
||||||
|
);
|
||||||
|
tracing::warn!("V8 heap limit reached: terminated the current JavaScript task");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_heap_checked<T>(&mut self, result: Result<T, String>) -> Result<T, String> {
|
||||||
|
if self.recover_heap_limit() {
|
||||||
|
Err("JavaScript heap limit exceeded; execution terminated".to_string())
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_runtime_script(
|
||||||
|
&mut self,
|
||||||
|
name: &'static str,
|
||||||
|
source: String,
|
||||||
|
) -> Result<deno_core::v8::Global<deno_core::v8::Value>, String> {
|
||||||
|
let result = self
|
||||||
|
.runtime
|
||||||
|
.execute_script(name, source)
|
||||||
|
.map_err(|error| error.to_string());
|
||||||
|
self.finish_heap_checked(result)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse and merge an inline document import map. Rules which would alter
|
/// Parse and merge an inline document import map. Rules which would alter
|
||||||
/// already-observed module resolutions are discarded while unrelated new
|
/// already-observed module resolutions are discarded while unrelated new
|
||||||
/// rules remain available, matching Chromium's multiple-map model.
|
/// rules remain available, matching Chromium's multiple-map model.
|
||||||
@@ -406,7 +523,7 @@ impl ObscuraJsRuntime {
|
|||||||
|
|
||||||
pub fn set_user_agent(&mut self, ua: &str) {
|
pub fn set_user_agent(&mut self, ua: &str) {
|
||||||
let escaped = ua.replace('\\', "\\\\").replace('\'', "\\'");
|
let escaped = ua.replace('\\', "\\\\").replace('\'', "\\'");
|
||||||
let _ = self.runtime.execute_script(
|
let _ = self.execute_runtime_script(
|
||||||
"<set-ua>",
|
"<set-ua>",
|
||||||
format!("globalThis.__obscura_ua = '{}';", escaped),
|
format!("globalThis.__obscura_ua = '{}';", escaped),
|
||||||
);
|
);
|
||||||
@@ -416,7 +533,7 @@ impl ObscuraJsRuntime {
|
|||||||
let p = platform.replace('\'', "\\'");
|
let p = platform.replace('\'', "\\'");
|
||||||
let uap = ua_platform.replace('\'', "\\'");
|
let uap = ua_platform.replace('\'', "\\'");
|
||||||
let uapv = ua_platform_version.replace('\'', "\\'");
|
let uapv = ua_platform_version.replace('\'', "\\'");
|
||||||
let _ = self.runtime.execute_script(
|
let _ = self.execute_runtime_script(
|
||||||
"<set-platform>",
|
"<set-platform>",
|
||||||
format!(
|
format!(
|
||||||
"globalThis.__obscura_platform='{}';globalThis.__obscura_ua_platform='{}';globalThis.__obscura_ua_platform_version='{}';",
|
"globalThis.__obscura_platform='{}';globalThis.__obscura_ua_platform='{}';globalThis.__obscura_ua_platform_version='{}';",
|
||||||
@@ -426,7 +543,7 @@ impl ObscuraJsRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_stealth(&mut self, enabled: bool) {
|
pub fn set_stealth(&mut self, enabled: bool) {
|
||||||
let _ = self.runtime.execute_script(
|
let _ = self.execute_runtime_script(
|
||||||
"<set-stealth>",
|
"<set-stealth>",
|
||||||
format!("globalThis.__obscura_stealth = {};", enabled),
|
format!("globalThis.__obscura_stealth = {};", enabled),
|
||||||
);
|
);
|
||||||
@@ -450,7 +567,7 @@ impl ObscuraJsRuntime {
|
|||||||
state.resolved_scroll = None;
|
state.resolved_scroll = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = self.runtime.execute_script(
|
let _ = self.execute_runtime_script(
|
||||||
"<set-viewport>",
|
"<set-viewport>",
|
||||||
format!(
|
format!(
|
||||||
"globalThis.__obscura_viewport_w={width};\
|
"globalThis.__obscura_viewport_w={width};\
|
||||||
@@ -490,7 +607,7 @@ impl ObscuraJsRuntime {
|
|||||||
"globalThis.__obscura_set_screen_override(null,null,{emulated});"
|
"globalThis.__obscura_set_screen_override(null,null,{emulated});"
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
let _ = self.runtime.execute_script("<set-screen-size>", script);
|
let _ = self.execute_runtime_script("<set-screen-size>", script);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Current clamped root scroll offset shared by CSSOM geometry and paint.
|
/// Current clamped root scroll offset shared by CSSOM geometry and paint.
|
||||||
@@ -916,7 +1033,7 @@ impl ObscuraJsRuntime {
|
|||||||
/// Run __obscura_init() after all per-page properties (UA, platform, stealth, etc.)
|
/// Run __obscura_init() after all per-page properties (UA, platform, stealth, etc.)
|
||||||
/// have been set. Must be called once per page setup, after all set_* methods.
|
/// have been set. Must be called once per page setup, after all set_* methods.
|
||||||
pub fn run_page_init(&mut self) {
|
pub fn run_page_init(&mut self) {
|
||||||
let _ = self.runtime.execute_script(
|
let _ = self.execute_runtime_script(
|
||||||
"<obscura:page-init>",
|
"<obscura:page-init>",
|
||||||
"globalThis.__obscura_init();".to_string(),
|
"globalThis.__obscura_init();".to_string(),
|
||||||
);
|
);
|
||||||
@@ -926,7 +1043,7 @@ impl ObscuraJsRuntime {
|
|||||||
/// values are injected as numeric globals the bootstrap reads; when unset it
|
/// values are injected as numeric globals the bootstrap reads; when unset it
|
||||||
/// keeps the built-in default. Callers validate the range before calling.
|
/// keeps the built-in default. Callers validate the range before calling.
|
||||||
pub fn set_geolocation(&mut self, latitude: f64, longitude: f64) {
|
pub fn set_geolocation(&mut self, latitude: f64, longitude: f64) {
|
||||||
let _ = self.runtime.execute_script(
|
let _ = self.execute_runtime_script(
|
||||||
"<set-geo>",
|
"<set-geo>",
|
||||||
format!(
|
format!(
|
||||||
"globalThis.__obscura_geo_lat={};globalThis.__obscura_geo_lon={};",
|
"globalThis.__obscura_geo_lat={};globalThis.__obscura_geo_lon={};",
|
||||||
@@ -939,8 +1056,7 @@ impl ObscuraJsRuntime {
|
|||||||
self.begin_javascript_task();
|
self.begin_javascript_task();
|
||||||
let wrapped = Self::wrap_expression(expression);
|
let wrapped = Self::wrap_expression(expression);
|
||||||
let result = self
|
let result = self
|
||||||
.runtime
|
.execute_runtime_script("<eval>", wrapped)
|
||||||
.execute_script("<eval>", wrapped)
|
|
||||||
.map_err(|e| format!("JS error: {}", e))?;
|
.map_err(|e| format!("JS error: {}", e))?;
|
||||||
self.v8_to_json(result)
|
self.v8_to_json(result)
|
||||||
}
|
}
|
||||||
@@ -1025,8 +1141,7 @@ impl ObscuraJsRuntime {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = self
|
let result = self
|
||||||
.runtime
|
.execute_runtime_script("<eval-remote>", meta_code)
|
||||||
.execute_script("<eval-remote>", meta_code)
|
|
||||||
.map_err(|e| format!("JS error: {}", e))?;
|
.map_err(|e| format!("JS error: {}", e))?;
|
||||||
|
|
||||||
let meta_str = if await_promise {
|
let meta_str = if await_promise {
|
||||||
@@ -1035,8 +1150,7 @@ impl ObscuraJsRuntime {
|
|||||||
let settled = self
|
let settled = self
|
||||||
.resolve_promises_until(
|
.resolve_promises_until(
|
||||||
|rt| {
|
|rt| {
|
||||||
rt.runtime
|
rt.execute_runtime_script("<done?>", sentinel.clone())
|
||||||
.execute_script("<done?>", sentinel.clone())
|
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| rt.v8_to_json(v).ok())
|
.and_then(|v| rt.v8_to_json(v).ok())
|
||||||
.and_then(|j| j.as_bool())
|
.and_then(|j| j.as_bool())
|
||||||
@@ -1064,22 +1178,20 @@ impl ObscuraJsRuntime {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let rejected = self
|
let rejected = self
|
||||||
.runtime
|
.execute_runtime_script(
|
||||||
.execute_script(
|
|
||||||
"<readRejected>",
|
"<readRejected>",
|
||||||
"globalThis.__obscura_await_rejected".to_string(),
|
"globalThis.__obscura_await_rejected".to_string(),
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("JS error: {}", e))?;
|
.map_err(|e| format!("JS error: {}", e))?;
|
||||||
if self.v8_to_json(rejected)?.as_bool().unwrap_or(false) {
|
if self.v8_to_json(rejected)?.as_bool().unwrap_or(false) {
|
||||||
let err = self.runtime.execute_script("<readError>", format!("String(globalThis.__obscura_objects['{0}'] && (globalThis.__obscura_objects['{0}'].message || globalThis.__obscura_objects['{0}']))", oid))
|
let err = self.execute_runtime_script("<readError>", format!("String(globalThis.__obscura_objects['{0}'] && (globalThis.__obscura_objects['{0}'].message || globalThis.__obscura_objects['{0}']))", oid))
|
||||||
.map_err(|e| format!("JS error: {}", e))?;
|
.map_err(|e| format!("JS error: {}", e))?;
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Promise rejected: {}",
|
"Promise rejected: {}",
|
||||||
self.v8_to_json(err)?.as_str().unwrap_or("")
|
self.v8_to_json(err)?.as_str().unwrap_or("")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
self.runtime
|
self.execute_runtime_script("<readMeta>", "globalThis.__obscura_await_meta".to_string())
|
||||||
.execute_script("<readMeta>", "globalThis.__obscura_await_meta".to_string())
|
|
||||||
.map_err(|e| format!("JS error: {}", e))?
|
.map_err(|e| format!("JS error: {}", e))?
|
||||||
} else {
|
} else {
|
||||||
result
|
result
|
||||||
@@ -1097,8 +1209,7 @@ impl ObscuraJsRuntime {
|
|||||||
|
|
||||||
if await_promise && return_by_value {
|
if await_promise && return_by_value {
|
||||||
let read = self
|
let read = self
|
||||||
.runtime
|
.execute_runtime_script(
|
||||||
.execute_script(
|
|
||||||
"<readResult>",
|
"<readResult>",
|
||||||
format!("globalThis.__obscura_objects['{}']", oid),
|
format!("globalThis.__obscura_objects['{}']", oid),
|
||||||
)
|
)
|
||||||
@@ -1176,8 +1287,7 @@ impl ObscuraJsRuntime {
|
|||||||
done_counter = done_counter,
|
done_counter = done_counter,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.runtime
|
self.execute_runtime_script("<callFnAsync>", code)
|
||||||
.execute_script("<callFnAsync>", code)
|
|
||||||
.map_err(|e| format!("JS error: {}", e))?;
|
.map_err(|e| format!("JS error: {}", e))?;
|
||||||
|
|
||||||
let __t0 = std::time::Instant::now();
|
let __t0 = std::time::Instant::now();
|
||||||
@@ -1185,8 +1295,7 @@ impl ObscuraJsRuntime {
|
|||||||
let settled = self
|
let settled = self
|
||||||
.resolve_promises_until(
|
.resolve_promises_until(
|
||||||
|rt| {
|
|rt| {
|
||||||
rt.runtime
|
rt.execute_runtime_script("<done?>", sentinel.clone())
|
||||||
.execute_script("<done?>", sentinel.clone())
|
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| rt.v8_to_json(v).ok())
|
.and_then(|v| rt.v8_to_json(v).ok())
|
||||||
.and_then(|j| j.as_bool())
|
.and_then(|j| j.as_bool())
|
||||||
@@ -1216,8 +1325,7 @@ impl ObscuraJsRuntime {
|
|||||||
|
|
||||||
if return_by_value {
|
if return_by_value {
|
||||||
let read = self
|
let read = self
|
||||||
.runtime
|
.execute_runtime_script(
|
||||||
.execute_script(
|
|
||||||
"<readResult>",
|
"<readResult>",
|
||||||
format!("globalThis.__obscura_objects['{}']", oid),
|
format!("globalThis.__obscura_objects['{}']", oid),
|
||||||
)
|
)
|
||||||
@@ -1227,8 +1335,7 @@ impl ObscuraJsRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let meta_result = self
|
let meta_result = self
|
||||||
.runtime
|
.execute_runtime_script("<readMeta>", "globalThis.__obscura_await_meta".to_string())
|
||||||
.execute_script("<readMeta>", "globalThis.__obscura_await_meta".to_string())
|
|
||||||
.map_err(|e| format!("JS error: {}", e))?;
|
.map_err(|e| format!("JS error: {}", e))?;
|
||||||
let meta_str = self.v8_to_json(meta_result)?;
|
let meta_str = self.v8_to_json(meta_result)?;
|
||||||
let meta_json = if let serde_json::Value::String(s) = &meta_str {
|
let meta_json = if let serde_json::Value::String(s) = &meta_str {
|
||||||
@@ -1257,8 +1364,7 @@ impl ObscuraJsRuntime {
|
|||||||
args = args_list,
|
args = args_list,
|
||||||
);
|
);
|
||||||
let result = self
|
let result = self
|
||||||
.runtime
|
.execute_runtime_script("<callFnByValue>", code)
|
||||||
.execute_script("<callFnByValue>", code)
|
|
||||||
.map_err(|e| format!("JS error: {}", e))?;
|
.map_err(|e| format!("JS error: {}", e))?;
|
||||||
let json_val = self.v8_to_json(result)?;
|
let json_val = self.v8_to_json(result)?;
|
||||||
return Ok(Self::info_from_json(&json_val));
|
return Ok(Self::info_from_json(&json_val));
|
||||||
@@ -1281,8 +1387,7 @@ impl ObscuraJsRuntime {
|
|||||||
meta_fn = Self::meta_extract_js("__result"),
|
meta_fn = Self::meta_extract_js("__result"),
|
||||||
);
|
);
|
||||||
let result = self
|
let result = self
|
||||||
.runtime
|
.execute_runtime_script("<callFnRemote>", code)
|
||||||
.execute_script("<callFnRemote>", code)
|
|
||||||
.map_err(|e| format!("JS error: {}", e))?;
|
.map_err(|e| format!("JS error: {}", e))?;
|
||||||
let meta_str = self.v8_to_json(result)?;
|
let meta_str = self.v8_to_json(result)?;
|
||||||
let meta_json = if let serde_json::Value::String(s) = &meta_str {
|
let meta_json = if let serde_json::Value::String(s) = &meta_str {
|
||||||
@@ -1320,8 +1425,7 @@ impl ObscuraJsRuntime {
|
|||||||
"globalThis.__obscura_objects['{}'] = ({});",
|
"globalThis.__obscura_objects['{}'] = ({});",
|
||||||
oid, js_expression,
|
oid, js_expression,
|
||||||
);
|
);
|
||||||
self.runtime
|
self.execute_runtime_script("<store>", code)
|
||||||
.execute_script("<store>", code)
|
|
||||||
.map_err(|e| format!("Store error: {}", e))?;
|
.map_err(|e| format!("Store error: {}", e))?;
|
||||||
self.object_store.insert(
|
self.object_store.insert(
|
||||||
oid.clone(),
|
oid.clone(),
|
||||||
@@ -1348,8 +1452,7 @@ impl ObscuraJsRuntime {
|
|||||||
meta_fn = Self::meta_extract_js("__result"),
|
meta_fn = Self::meta_extract_js("__result"),
|
||||||
);
|
);
|
||||||
let result = self
|
let result = self
|
||||||
.runtime
|
.execute_runtime_script("<store-meta>", code)
|
||||||
.execute_script("<store-meta>", code)
|
|
||||||
.map_err(|e| format!("Store error: {}", e))?;
|
.map_err(|e| format!("Store error: {}", e))?;
|
||||||
let meta_str = self.v8_to_json(result)?;
|
let meta_str = self.v8_to_json(result)?;
|
||||||
let meta_json = if let serde_json::Value::String(s) = &meta_str {
|
let meta_json = if let serde_json::Value::String(s) = &meta_str {
|
||||||
@@ -1367,12 +1470,12 @@ impl ObscuraJsRuntime {
|
|||||||
pub fn release_object(&mut self, object_id: &str) {
|
pub fn release_object(&mut self, object_id: &str) {
|
||||||
if self.object_store.remove(object_id).is_some() {
|
if self.object_store.remove(object_id).is_some() {
|
||||||
let code = format!("delete globalThis.__obscura_objects['{}'];", object_id,);
|
let code = format!("delete globalThis.__obscura_objects['{}'];", object_id,);
|
||||||
let _ = self.runtime.execute_script("<release>", code);
|
let _ = self.execute_runtime_script("<release>", code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn release_object_group(&mut self) {
|
pub fn release_object_group(&mut self) {
|
||||||
let _ = self.runtime.execute_script(
|
let _ = self.execute_runtime_script(
|
||||||
"<releaseGroup>",
|
"<releaseGroup>",
|
||||||
"globalThis.__obscura_objects = {};".to_string(),
|
"globalThis.__obscura_objects = {};".to_string(),
|
||||||
);
|
);
|
||||||
@@ -1398,6 +1501,7 @@ impl ObscuraJsRuntime {
|
|||||||
let budget = tokio::time::Duration::from_millis(budget_ms);
|
let budget = tokio::time::Duration::from_millis(budget_ms);
|
||||||
let specifier = deno_core::ModuleSpecifier::parse(url)
|
let specifier = deno_core::ModuleSpecifier::parse(url)
|
||||||
.map_err(|e| format!("Invalid module URL {}: {}", url, e))?;
|
.map_err(|e| format!("Invalid module URL {}: {}", url, e))?;
|
||||||
|
let loaded_start = self.loaded_module_specifiers.borrow().len();
|
||||||
|
|
||||||
// Bound the recursive import-graph fetch. deno_core fetches the graph
|
// Bound the recursive import-graph fetch. deno_core fetches the graph
|
||||||
// concurrently through the one page-scoped module loader. Loading the
|
// concurrently through the one page-scoped module loader. Loading the
|
||||||
@@ -1425,9 +1529,16 @@ impl ObscuraJsRuntime {
|
|||||||
// Return as soon as the module finishes evaluating rather than waiting
|
// Return as soon as the module finishes evaluating rather than waiting
|
||||||
// for the loop to go fully idle: a page timer (setInterval) keeps the
|
// for the loop to go fully idle: a page timer (setInterval) keeps the
|
||||||
// loop busy forever and would otherwise burn the whole budget (#374).
|
// loop busy forever and would otherwise burn the whole budget (#374).
|
||||||
|
let mut graph_specifiers = self.loaded_module_specifiers.borrow()[loaded_start..].to_vec();
|
||||||
|
graph_specifiers.push(specifier.to_string());
|
||||||
|
graph_specifiers.sort_unstable();
|
||||||
|
graph_specifiers.dedup();
|
||||||
|
|
||||||
Ok(PreparedModule {
|
Ok(PreparedModule {
|
||||||
module_id,
|
module_id,
|
||||||
description: format!("Module {}", url),
|
description: format!("Module {}", url),
|
||||||
|
entry_specifier: Some(specifier.to_string()),
|
||||||
|
graph_specifiers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1447,9 +1558,34 @@ impl ObscuraJsRuntime {
|
|||||||
budget_ms: u64,
|
budget_ms: u64,
|
||||||
what: &str,
|
what: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
if let Some(outcome) = self.module_evaluations.get(&module_id) {
|
||||||
|
return outcome.clone();
|
||||||
|
}
|
||||||
|
|
||||||
self.begin_javascript_task();
|
self.begin_javascript_task();
|
||||||
let budget = tokio::time::Duration::from_millis(budget_ms);
|
let budget = tokio::time::Duration::from_millis(budget_ms);
|
||||||
let result = self.runtime.mod_evaluate(module_id);
|
// deno_core 0.350 asserts instead of treating a second evaluation as
|
||||||
|
// the module-map no-op required by browsers. The local outcome cache
|
||||||
|
// covers duplicate roots prepared by Obscura. A root can also have
|
||||||
|
// been evaluated earlier as another graph's dependency, which is only
|
||||||
|
// observable when mod_evaluate checks V8's private module status, so
|
||||||
|
// contain that dependency assertion at this boundary as well.
|
||||||
|
let evaluation = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
self.runtime.mod_evaluate(module_id)
|
||||||
|
}));
|
||||||
|
let result = match evaluation {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(payload) => {
|
||||||
|
let message = panic_payload_message(payload.as_ref());
|
||||||
|
let outcome = if message.contains("Module already evaluated") {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("{} evaluation panicked: {}", what, message))
|
||||||
|
};
|
||||||
|
self.module_evaluations.insert(module_id, outcome.clone());
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
};
|
||||||
tokio::pin!(result);
|
tokio::pin!(result);
|
||||||
|
|
||||||
let outcome = tokio::time::timeout(budget, async {
|
let outcome = tokio::time::timeout(budget, async {
|
||||||
@@ -1465,14 +1601,17 @@ impl ObscuraJsRuntime {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match outcome {
|
let outcome = match outcome {
|
||||||
Ok(Ok(())) => Ok(()),
|
Ok(Ok(())) => Ok(()),
|
||||||
Ok(Err(e)) => Err(format!("{} eval error: {}", what, e)),
|
Ok(Err(e)) => Err(format!("{} eval error: {}", what, e)),
|
||||||
Err(_) => Err(format!(
|
Err(_) => Err(format!(
|
||||||
"{} evaluation timed out after {}ms",
|
"{} evaluation timed out after {}ms",
|
||||||
what, budget_ms
|
what, budget_ms
|
||||||
)),
|
)),
|
||||||
}
|
};
|
||||||
|
let outcome = self.finish_heap_checked(outcome);
|
||||||
|
self.module_evaluations.insert(module_id, outcome.clone());
|
||||||
|
outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_inline_module(
|
pub async fn load_inline_module(
|
||||||
@@ -1508,6 +1647,7 @@ impl ObscuraJsRuntime {
|
|||||||
// each prepared module distinct until its scheduled evaluation.
|
// each prepared module distinct until its scheduled evaluation.
|
||||||
let specifier = deno_core::ModuleSpecifier::parse(base_url)
|
let specifier = deno_core::ModuleSpecifier::parse(base_url)
|
||||||
.unwrap_or_else(|_| deno_core::ModuleSpecifier::parse("about:blank").unwrap());
|
.unwrap_or_else(|_| deno_core::ModuleSpecifier::parse("about:blank").unwrap());
|
||||||
|
let loaded_start = self.loaded_module_specifiers.borrow().len();
|
||||||
|
|
||||||
let module_id = match tokio::time::timeout(
|
let module_id = match tokio::time::timeout(
|
||||||
budget,
|
budget,
|
||||||
@@ -1533,9 +1673,17 @@ impl ObscuraJsRuntime {
|
|||||||
// keeps the loop busy forever, and waiting for idle burned the whole
|
// keeps the loop busy forever, and waiting for idle burned the whole
|
||||||
// budget on this preamble module and starved the module that mounts the
|
// budget on this preamble module and starved the module that mounts the
|
||||||
// app, leaving #root empty (issue #374).
|
// app, leaving #root empty (issue #374).
|
||||||
|
let mut graph_specifiers = self.loaded_module_specifiers.borrow()[loaded_start..].to_vec();
|
||||||
|
graph_specifiers.sort_unstable();
|
||||||
|
graph_specifiers.dedup();
|
||||||
|
|
||||||
Ok(PreparedModule {
|
Ok(PreparedModule {
|
||||||
module_id,
|
module_id,
|
||||||
description: "Inline module".to_string(),
|
description: "Inline module".to_string(),
|
||||||
|
// Multiple inline modules intentionally share the document URL,
|
||||||
|
// but each has its own source and ModuleId.
|
||||||
|
entry_specifier: None,
|
||||||
|
graph_specifiers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1547,7 +1695,15 @@ impl ObscuraJsRuntime {
|
|||||||
let PreparedModule {
|
let PreparedModule {
|
||||||
module_id,
|
module_id,
|
||||||
description,
|
description,
|
||||||
|
entry_specifier,
|
||||||
|
graph_specifiers,
|
||||||
} = prepared;
|
} = prepared;
|
||||||
|
if let Some(outcome) = entry_specifier
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|specifier| self.evaluated_module_specifiers.get(specifier))
|
||||||
|
{
|
||||||
|
return outcome.clone();
|
||||||
|
}
|
||||||
// Tokio timeouts cannot run while synchronous top-level module work
|
// Tokio timeouts cannot run while synchronous top-level module work
|
||||||
// pins the runtime thread in V8. Pair the async timeout with a hard V8
|
// pins the runtime thread in V8. Pair the async timeout with a hard V8
|
||||||
// watchdog so this budget is a real wall-clock ceiling for both forms
|
// watchdog so this budget is a real wall-clock ceiling for both forms
|
||||||
@@ -1557,14 +1713,25 @@ impl ObscuraJsRuntime {
|
|||||||
.drive_module_eval(module_id, budget_ms, &description)
|
.drive_module_eval(module_id, budget_ms, &description)
|
||||||
.await;
|
.await;
|
||||||
let watchdog_fired = self.disarm_watchdog(watchdog);
|
let watchdog_fired = self.disarm_watchdog(watchdog);
|
||||||
if watchdog_fired {
|
let result = if watchdog_fired {
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"{} evaluation timed out after {}ms",
|
"{} evaluation timed out after {}ms",
|
||||||
description, budget_ms
|
description, budget_ms
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
result
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(entry_specifier) = entry_specifier {
|
||||||
|
self.evaluated_module_specifiers
|
||||||
|
.insert(entry_specifier, result.clone());
|
||||||
}
|
}
|
||||||
|
if result.is_ok() {
|
||||||
|
for specifier in graph_specifiers {
|
||||||
|
self.evaluated_module_specifiers.insert(specifier, Ok(()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_classic_script(&mut self, name: &str, source: &str) -> Result<(), String> {
|
fn execute_classic_script(&mut self, name: &str, source: &str) -> Result<(), String> {
|
||||||
@@ -1573,53 +1740,60 @@ impl ObscuraJsRuntime {
|
|||||||
// &'static str. Browser script URLs are runtime data, and V8 uses this
|
// &'static str. Browser script URLs are runtime data, and V8 uses this
|
||||||
// origin as import()'s referrer, so compile in the runtime's main
|
// origin as import()'s referrer, so compile in the runtime's main
|
||||||
// context directly instead of substituting the fixed "<script>" name.
|
// context directly instead of substituting the fixed "<script>" name.
|
||||||
let scope = &mut self.runtime.handle_scope();
|
let result = (|| {
|
||||||
let source = deno_core::v8::String::new(scope, source)
|
let scope = &mut self.runtime.handle_scope();
|
||||||
.ok_or_else(|| "JS error: source allocation failed".to_string())?;
|
let source = deno_core::v8::String::new(scope, source)
|
||||||
let name = deno_core::v8::String::new(scope, name)
|
.ok_or_else(|| "JS error: source allocation failed".to_string())?;
|
||||||
.ok_or_else(|| "JS error: script URL allocation failed".to_string())?;
|
let name = deno_core::v8::String::new(scope, name)
|
||||||
let origin = deno_core::v8::ScriptOrigin::new(
|
.ok_or_else(|| "JS error: script URL allocation failed".to_string())?;
|
||||||
scope,
|
let origin = deno_core::v8::ScriptOrigin::new(
|
||||||
name.into(),
|
scope,
|
||||||
0,
|
name.into(),
|
||||||
0,
|
0,
|
||||||
false,
|
0,
|
||||||
0,
|
false,
|
||||||
None,
|
0,
|
||||||
false,
|
None,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
None,
|
false,
|
||||||
);
|
None,
|
||||||
let scope = &mut deno_core::v8::TryCatch::new(scope);
|
);
|
||||||
let script = deno_core::v8::Script::compile(scope, source, Some(&origin));
|
let scope = &mut deno_core::v8::TryCatch::new(scope);
|
||||||
let Some(script) = script else {
|
let script = deno_core::v8::Script::compile(scope, source, Some(&origin));
|
||||||
if scope.is_execution_terminating() {
|
let Some(script) = script else {
|
||||||
scope.cancel_terminate_execution();
|
if scope.is_execution_terminating() {
|
||||||
return Err("JS error: Uncaught Error: execution terminated".to_string());
|
scope.cancel_terminate_execution();
|
||||||
}
|
return Err("JS error: Uncaught Error: execution terminated".to_string());
|
||||||
return match scope.exception() {
|
|
||||||
Some(exception) => {
|
|
||||||
let error = deno_core::error::JsError::from_v8_exception(scope, exception);
|
|
||||||
Err(format!("JS error: {error}"))
|
|
||||||
}
|
}
|
||||||
None => Err("JS error: script compilation failed without an exception".to_string()),
|
return match scope.exception() {
|
||||||
|
Some(exception) => {
|
||||||
|
let error = deno_core::error::JsError::from_v8_exception(scope, exception);
|
||||||
|
Err(format!("JS error: {error}"))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
Err("JS error: script compilation failed without an exception".to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
if script.run(scope).is_none() {
|
||||||
if script.run(scope).is_none() {
|
if scope.is_execution_terminating() {
|
||||||
if scope.is_execution_terminating() {
|
scope.cancel_terminate_execution();
|
||||||
scope.cancel_terminate_execution();
|
return Err("JS error: Uncaught Error: execution terminated".to_string());
|
||||||
return Err("JS error: Uncaught Error: execution terminated".to_string());
|
|
||||||
}
|
|
||||||
return match scope.exception() {
|
|
||||||
Some(exception) => {
|
|
||||||
let error = deno_core::error::JsError::from_v8_exception(scope, exception);
|
|
||||||
Err(format!("JS error: {error}"))
|
|
||||||
}
|
}
|
||||||
None => Err("JS error: script execution failed without an exception".to_string()),
|
return match scope.exception() {
|
||||||
};
|
Some(exception) => {
|
||||||
}
|
let error = deno_core::error::JsError::from_v8_exception(scope, exception);
|
||||||
Ok(())
|
Err(format!("JS error: {error}"))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
Err("JS error: script execution failed without an exception".to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
self.finish_heap_checked(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_script(&mut self, name: &str, source: &str) -> Result<(), String> {
|
pub fn execute_script(&mut self, name: &str, source: &str) -> Result<(), String> {
|
||||||
@@ -1706,7 +1880,7 @@ impl ObscuraJsRuntime {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Event loop error: {}", e));
|
.map_err(|e| format!("Event loop error: {}", e));
|
||||||
self.runtime.v8_isolate().perform_microtask_checkpoint();
|
self.runtime.v8_isolate().perform_microtask_checkpoint();
|
||||||
result
|
self.finish_heap_checked(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the serialized dynamic-script queue is still fetching or
|
/// Whether the serialized dynamic-script queue is still fetching or
|
||||||
@@ -1850,6 +2024,7 @@ impl ObscuraJsRuntime {
|
|||||||
};
|
};
|
||||||
let fired = self.disarm_watchdog(token);
|
let fired = self.disarm_watchdog(token);
|
||||||
match result {
|
match result {
|
||||||
|
Err(error) if error.contains("heap limit exceeded") => Err(error),
|
||||||
Err(error) if fired || error.contains("execution terminated") => Ok(()),
|
Err(error) if fired || error.contains("execution terminated") => Ok(()),
|
||||||
other => other,
|
other => other,
|
||||||
}
|
}
|
||||||
@@ -1887,7 +2062,7 @@ impl ObscuraJsRuntime {
|
|||||||
self.begin_javascript_task();
|
self.begin_javascript_task();
|
||||||
self.runtime.v8_isolate().perform_microtask_checkpoint();
|
self.runtime.v8_isolate().perform_microtask_checkpoint();
|
||||||
let mut waiting_for_wake = false;
|
let mut waiting_for_wake = false;
|
||||||
std::future::poll_fn(|cx| {
|
let result = std::future::poll_fn(|cx| {
|
||||||
let tick = self
|
let tick = self
|
||||||
.runtime
|
.runtime
|
||||||
.poll_event_loop(cx, deno_core::PollEventLoopOptions::default());
|
.poll_event_loop(cx, deno_core::PollEventLoopOptions::default());
|
||||||
@@ -1905,7 +2080,8 @@ impl ObscuraJsRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
|
self.finish_heap_checked(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drive one browser task while allowing the future to remain parked on
|
/// Drive one browser task while allowing the future to remain parked on
|
||||||
@@ -1935,10 +2111,13 @@ impl ObscuraJsRuntime {
|
|||||||
self.cancel_termination();
|
self.cancel_termination();
|
||||||
return Err("autonomous microtask checkpoint exceeded its task budget".into());
|
return Err("autonomous microtask checkpoint exceeded its task budget".into());
|
||||||
}
|
}
|
||||||
|
if self.recover_heap_limit() {
|
||||||
|
return Err("JavaScript heap limit exceeded; execution terminated".into());
|
||||||
|
}
|
||||||
|
|
||||||
let isolate_handle = self.isolate_handle();
|
let isolate_handle = self.isolate_handle();
|
||||||
let mut waiting_for_wake = false;
|
let mut waiting_for_wake = false;
|
||||||
std::future::poll_fn(|cx| {
|
let result = std::future::poll_fn(|cx| {
|
||||||
let watchdog = crate::cdp_watchdog::arm(
|
let watchdog = crate::cdp_watchdog::arm(
|
||||||
isolate_handle.clone(),
|
isolate_handle.clone(),
|
||||||
std::time::Duration::from_millis(AUTONOMOUS_TASK_WATCHDOG_MS),
|
std::time::Duration::from_millis(AUTONOMOUS_TASK_WATCHDOG_MS),
|
||||||
@@ -1967,7 +2146,8 @@ impl ObscuraJsRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
|
self.finish_heap_checked(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drive one cooperative event-loop turn for browser lifecycle code that
|
/// Drive one cooperative event-loop turn for browser lifecycle code that
|
||||||
@@ -2098,6 +2278,7 @@ impl ObscuraJsRuntime {
|
|||||||
};
|
};
|
||||||
let fired = self.disarm_watchdog(token);
|
let fired = self.disarm_watchdog(token);
|
||||||
match result {
|
match result {
|
||||||
|
Err(error) if error.contains("heap limit exceeded") => Err(error),
|
||||||
Err(error) if fired || error.contains("execution terminated") => Ok(()),
|
Err(error) if fired || error.contains("execution terminated") => Ok(()),
|
||||||
other => other,
|
other => other,
|
||||||
}
|
}
|
||||||
@@ -2119,6 +2300,9 @@ impl ObscuraJsRuntime {
|
|||||||
let token = self.arm_watchdog(timeout);
|
let token = self.arm_watchdog(timeout);
|
||||||
let result = self.runtime.execute_script("<eval>", wrapped);
|
let result = self.runtime.execute_script("<eval>", wrapped);
|
||||||
let fired = self.disarm_watchdog(token);
|
let fired = self.disarm_watchdog(token);
|
||||||
|
if self.recover_heap_limit() {
|
||||||
|
return Err("JavaScript heap limit exceeded; execution terminated".to_string());
|
||||||
|
}
|
||||||
match result {
|
match result {
|
||||||
Ok(v) if !fired => self.v8_to_json(v),
|
Ok(v) if !fired => self.v8_to_json(v),
|
||||||
Ok(_) => Err("eval timed out".to_string()),
|
Ok(_) => Err("eval timed out".to_string()),
|
||||||
@@ -2142,6 +2326,7 @@ impl ObscuraJsRuntime {
|
|||||||
.run_event_loop(deno_core::PollEventLoopOptions::default()),
|
.run_event_loop(deno_core::PollEventLoopOptions::default()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
self.recover_heap_limit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pump the event loop until `done_check` returns true (e.g. an IIFE
|
/// Pump the event loop until `done_check` returns true (e.g. an IIFE
|
||||||
@@ -2183,6 +2368,9 @@ impl ObscuraJsRuntime {
|
|||||||
.run_event_loop(deno_core::PollEventLoopOptions::default()),
|
.run_event_loop(deno_core::PollEventLoopOptions::default()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
if self.recover_heap_limit() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Backoff so a hung promise doesn't burn CPU. Caps at 50ms;
|
// Backoff so a hung promise doesn't burn CPU. Caps at 50ms;
|
||||||
// worst case we miss the result by <50ms.
|
// worst case we miss the result by <50ms.
|
||||||
if tick_ms < 50 {
|
if tick_ms < 50 {
|
||||||
@@ -14014,6 +14202,45 @@ mod tests {
|
|||||||
format!("http://{}", address)
|
format!("http://{}", address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn_duplicate_module_graph_server() -> String {
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let address = listener.local_addr().unwrap();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
use std::io::{Read as _, Write as _};
|
||||||
|
|
||||||
|
for _ in 0..2 {
|
||||||
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
|
let mut request = [0u8; 2048];
|
||||||
|
let length = stream.read(&mut request).unwrap();
|
||||||
|
let request = String::from_utf8_lossy(&request[..length]);
|
||||||
|
let path = request
|
||||||
|
.lines()
|
||||||
|
.next()
|
||||||
|
.and_then(|line| line.split_ascii_whitespace().nth(1))
|
||||||
|
.unwrap_or("/");
|
||||||
|
let body = match path {
|
||||||
|
"/entry.js" => {
|
||||||
|
"import './shared.js'; globalThis.__module_entry_ran = true;"
|
||||||
|
}
|
||||||
|
"/shared.js" => {
|
||||||
|
"globalThis.__shared_module_runs = \
|
||||||
|
(globalThis.__shared_module_runs || 0) + 1;"
|
||||||
|
}
|
||||||
|
_ => "throw new Error('unexpected module path');",
|
||||||
|
};
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\n\
|
||||||
|
Content-Type: application/javascript\r\n\
|
||||||
|
Content-Length: {}\r\n\
|
||||||
|
Connection: close\r\n\r\n{body}",
|
||||||
|
body.len(),
|
||||||
|
);
|
||||||
|
stream.write_all(response.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
format!("http://{}", address)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum ModuleGraphFixture {
|
enum ModuleGraphFixture {
|
||||||
CookieProtected,
|
CookieProtected,
|
||||||
@@ -14187,6 +14414,64 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn dependency_prepared_as_root_is_evaluated_only_once() {
|
||||||
|
let base = spawn_duplicate_module_graph_server();
|
||||||
|
let jar = std::sync::Arc::new(obscura_net::CookieJar::new());
|
||||||
|
let client = std::sync::Arc::new(obscura_net::ObscuraHttpClient::with_full_options(
|
||||||
|
jar, None, true,
|
||||||
|
));
|
||||||
|
let mut rt = ObscuraJsRuntime::with_base_url(&format!("{}/", base));
|
||||||
|
rt.set_http_client(client);
|
||||||
|
|
||||||
|
// The HTML scheduler prepares all module graphs before evaluating any
|
||||||
|
// of them. The shared URL is both a dependency and a later root, which
|
||||||
|
// used to reach deno_core::mod_evaluate twice and panic (#591).
|
||||||
|
let entry = rt
|
||||||
|
.prepare_module(&format!("{}/entry.js", base), 1_000)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let shared = rt
|
||||||
|
.prepare_module(&format!("{}/shared.js", base), 1_000)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
rt.evaluate_prepared_module(entry, 1_000).await.unwrap();
|
||||||
|
rt.evaluate_prepared_module(shared, 1_000).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rt.evaluate("globalThis.__module_entry_ran === true").unwrap(),
|
||||||
|
serde_json::json!(true),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rt.evaluate("globalThis.__shared_module_runs").unwrap(),
|
||||||
|
serde_json::json!(1.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn heap_limit_terminates_script_and_runtime_recovers() {
|
||||||
|
crate::v8_flags::set_v8_flags("--max-old-space-size=32 --max-semi-space-size=1");
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
|
||||||
|
for _ in 0..2 {
|
||||||
|
let error = rt
|
||||||
|
.evaluate(
|
||||||
|
"(() => { const chunks = []; for (;;) { \
|
||||||
|
chunks.push(new Array(262144).fill(1.25)); } })()",
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
error.contains("heap limit exceeded"),
|
||||||
|
"unexpected heap failure: {error}",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rt.evaluate("globalThis.__runtime_survived_oom = true").unwrap(),
|
||||||
|
serde_json::json!(true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn descendant_module_uses_page_cookie_identity_and_headers() {
|
async fn descendant_module_uses_page_cookie_identity_and_headers() {
|
||||||
let (base, requests) = spawn_module_graph_server(ModuleGraphFixture::CookieProtected);
|
let (base, requests) = spawn_module_graph_server(ModuleGraphFixture::CookieProtected);
|
||||||
|
|||||||
Reference in New Issue
Block a user