Obscura Reliability — V8 Watchdog, Panic Safety, and SSRF Protection
How Obscura ensures one bad page never hangs or crashes the engine. V8 termination watchdog, DOM panic safety, cycle guards, SSRF protection, and layered timeouts.
16Yun Engineering TeamJul 8, 20263 min read
One Bad Page Must Not Wedge the Service
A core design goal of Obscura: one bad page must never hang or crash the entire engine. Throw an infinite loop, an unresponsive API, or a deeply recursive DOM operation at Obscura, and it should recover gracefully.
This article covers the six-layer reliability system.
Layer 1: V8 Termination Watchdog
An infinite while(true){} in JavaScript cannot be interrupted by tokio::time::timeout because synchronous V8 work does not yield at await points.
Solution: a watchdog thread that terminates V8 from a separate thread.
fn arm_watchdog(isolate: &v8::Isolate, timeout_ms: u64) {
let isolate_handle = isolate.thread_safe_handle();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(timeout_ms));
isolate_handle.terminate_execution();
});
}
```bash
`terminate_execution()` throws an uncatchable exception, terminating the currently running JavaScript. Applies to `--eval`, post-load JavaScript microtask storms, and navigation event loop pumps.
A separate CDP watchdog (`cdp_watchdog.rs`) protects the shared V8 lock per CDP command, tunable via `OBSCURA_CDP_COMMAND_TIMEOUT_MS`.
## Layer 2: Panic Safety
Rust panics unwind the stack by default. If a panic occurs inside a V8 FFI frame, the unwind crosses the C++ boundary and **aborts the process**.
Solution: every DOM op is wrapped in `catch_unwind`:
```rust
let result = std::panic::catch_unwind(|| {
dom_tree.insert_before(parent_id, ref_id, new_id)
});
match result {
Ok(Ok(value)) => Ok(value),
Ok(Err(e)) => Err(e),
Err(_) => Ok(Value::Null), // panic caught, return null
}
```bash
This requires `panic = "unwind"` in the release profile (already pinned in the workspace Cargo.toml).
## Layer 3: DOM Cycle Guards
A real bug: `append_child` once allowed inserting an ancestor as a child node, making `descendants()` loop forever.
Fix: `append_child` and `insert_before` reject cyclic operations:
```rust
fn append_child(&mut self, parent: NodeId, child: NodeId) {
if self.is_ancestor_of(child, parent) {
return; // silently reject
}
// proceed with insertion
}
```bash
`descendants()` also has a hard-coded length cap as a final safeguard.
## Layer 4: SSRF Protection
By default, Obscura blocks:
- Loopback: `127.0.0.0/8`, `::1`
- RFC1918: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`
- Link-local: `169.254.0.0/16` (includes `169.254.169.254`)
- IPv6 unique-local: `fc00::/7`
- Unspecified: `0.0.0.0`, `::`
- All IPv4-mapped forms of the above
Validation runs twice:
1. At URL parse time
2. After DNS resolution (DNS rebinding protection)
Bypass with `--allow-private-network` or `OBSCURA_ALLOW_PRIVATE_NETWORK=1`.
## Layer 5: Fetch Timeout
Scripted `fetch()` and `XMLHttpRequest` are timeout-bounded:
```rust
let fetch_timeout = Duration::from_millis(OBSCURA_FETCH_TIMEOUT_MS);
let result = tokio::time::timeout(fetch_timeout, client.fetch(&url)).await;
```bash
Without this, a server that accepts but never responds would hang XHR permanently. Default: 30s.
## Layer 6: Process-Level Hard Deadline
The `fetch` command spawns a daemon thread that force-exits after `timeout + wait + 10` seconds:
```rust
let hard = Duration::from_secs(timeout_secs + wait_secs + 10);
std::thread::spawn(move || {
std::thread::sleep(hard);
std::process::exit(124); // final backstop
});
```bash
## Timeout Architecture
| Layer | Variable | Default | Scope |
|-------|----------|---------|-------|
| Navigation | `OBSCURA_NAV_TIMEOUT_MS` | 30s | `Page.navigate` + CLI `fetch` |
| CDP command | `OBSCURA_CDP_COMMAND_TIMEOUT_MS` | 60s | All CDP commands |
| Fetch/XHR | `OBSCURA_FETCH_TIMEOUT_MS` | 30s | JS fetch/XHR/module |
| Process (fetch only) | Programmatic | timeout+wait+10s | `fetch` subcommand |
## Summary
Obscura's reliability design is "a backstop at every layer":
1. **JS layer**: V8 watchdog terminates infinite loops
2. **Rust layer**: catch_unwind prevents panic propagation into V8 FFI
3. **DOM layer**: cycle guards + descendants length cap
4. **Network layer**: SSRF dual validation + fetch timeout + DNS rebinding protection
5. **Protocol layer**: CDP command watchdog prevents session lock
6. **Process layer**: Hard deadline thread for the fetch command
This system ensures that under most abnormal conditions, Obscura neither crashes nor hangs permanently.Need an enterprise proxy plan?
We can tailor architecture to your target domains, concurrency, and reliability goals.