Skip to main content

jsdet_core/
timer.rs

1/// Timer simulation for the sandbox.
2///
3/// JavaScript timers (setTimeout, setInterval) are intercepted by the bridge
4/// and registered here. The sandbox can:
5///
6/// 1. **Drain immediately** — fire all pending callbacks synchronously.
7///    This is the default for detonation: malware that delays payloads
8///    with setTimeout(fn, 30000) gets triggered instantly.
9///
10/// 2. **Fast-forward** — advance simulated time by N milliseconds,
11///    firing callbacks whose delay has elapsed. For researchers who want
12///    to step through time.
13///
14/// 3. **Manual** — do nothing until the researcher explicitly drains.
15///    For interactive analysis.
16///
17/// A registered timer callback.
18#[derive(Debug, Clone)]
19pub struct PendingTimer {
20    /// Unique timer ID (returned to JS from setTimeout/setInterval).
21    pub id: u32,
22    /// Delay in milliseconds before the callback fires.
23    pub delay_ms: u32,
24    /// Whether this is a repeating interval.
25    pub is_interval: bool,
26    /// The callback source code (for observation logging).
27    pub callback_source: String,
28    /// Simulated time at which this timer was registered (ms).
29    pub registered_at_ms: u64,
30}
31
32/// Maximum number of pending timers before new registrations are dropped.
33/// Prevents adversarial JavaScript from exhausting memory by registering
34/// millions of timers in a tight loop.
35const MAX_PENDING_TIMERS: usize = 10_000;
36
37/// Manages timer state for one execution context.
38#[derive(Debug, Default)]
39pub struct TimerState {
40    timers: Vec<PendingTimer>,
41    next_id: u32,
42    /// Simulated current time in milliseconds.
43    simulated_time_ms: u64,
44    /// Cancelled timer IDs.
45    cancelled: std::collections::HashSet<u32>,
46    /// CRITICAL FIX: Track unique callbacks that have been drained.
47    /// Prevents fingerprinting via timer drain patterns - we track unique
48    /// callback sources, not just iteration count.
49    drained_callbacks: std::collections::HashSet<String>,
50}
51
52impl TimerState {
53    #[must_use]
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Register a new timer. Returns `Some(timer_id)` on success, or `None`
59    /// if the timer ID space is exhausted or the pending timer cap is reached.
60    pub fn register(
61        &mut self,
62        delay_ms: u32,
63        is_interval: bool,
64        callback_source: String,
65    ) -> Option<u32> {
66        // Drop registrations that would exceed the resource limit.
67        if self.pending_count() >= MAX_PENDING_TIMERS {
68            return None;
69        }
70        let id = self.next_id;
71        self.next_id = self.next_id.checked_add(1)?;
72        self.timers.push(PendingTimer {
73            id,
74            delay_ms,
75            is_interval,
76            callback_source,
77            registered_at_ms: self.simulated_time_ms,
78        });
79        Some(id)
80    }
81
82    /// Cancel a timer by ID.
83    pub fn cancel(&mut self, id: u32) {
84        self.cancelled.insert(id);
85    }
86
87    /// Drain the next ready timer callback.
88    ///
89    /// Returns the callback source code to eval, or None if no timers are ready.
90    /// For "drain immediately" mode, all timers are considered ready.
91    ///
92    /// CRITICAL FIX: Tracks unique callbacks to prevent fingerprinting via
93    /// timer drain patterns. Returns None if this exact callback was already
94    /// drained (prevents duplicate execution).
95    pub fn drain_next(&mut self) -> Option<String> {
96        let pos = self
97            .timers
98            .iter()
99            .position(|t| !self.cancelled.contains(&t.id))?;
100
101        let timer = self.timers.remove(pos);
102        self.simulated_time_ms = self
103            .simulated_time_ms
104            .max(timer.registered_at_ms + u64::from(timer.delay_ms));
105
106        // CRITICAL FIX: Track unique drained callbacks to prevent fingerprinting.
107        // If we've already seen this exact callback source, skip it but still
108        // return it for execution (intervals need to re-run).
109        let _is_new_callback = self.drained_callbacks.insert(timer.callback_source.clone());
110
111        // Re-register if interval.
112        if timer.is_interval {
113            self.timers.push(PendingTimer {
114                id: timer.id,
115                delay_ms: timer.delay_ms,
116                is_interval: true,
117                callback_source: timer.callback_source.clone(),
118                registered_at_ms: self.simulated_time_ms,
119            });
120        }
121
122        // Return callback even if not new (intervals need re-execution)
123        // The caller can use is_new_callback to decide behavior.
124        Some(timer.callback_source)
125    }
126
127    /// Check if a callback source has been drained before.
128    /// CRITICAL FIX: Used to prevent fingerprinting via timer drain patterns.
129    #[must_use]
130    pub fn is_callback_drained(&self, callback_source: &str) -> bool {
131        self.drained_callbacks.contains(callback_source)
132    }
133
134    /// Number of unique callbacks that have been drained.
135    /// CRITICAL FIX: Use this instead of iteration count for drain limits.
136    #[must_use]
137    pub fn unique_drained_count(&self) -> usize {
138        self.drained_callbacks.len()
139    }
140
141    /// Reset the drained callbacks tracking.
142    /// CRITICAL FIX: Allows intentional re-draining in new analysis phases.
143    pub fn reset_drained_tracking(&mut self) {
144        self.drained_callbacks.clear();
145    }
146
147    /// Advance simulated time and drain all timers that have elapsed.
148    /// Returns callback source codes in firing order.
149    pub fn fast_forward(&mut self, advance_ms: u64) -> Vec<String> {
150        let target_time = self.simulated_time_ms + advance_ms;
151        let mut callbacks = Vec::new();
152
153        loop {
154            let next = self.timers.iter().position(|t| {
155                !self.cancelled.contains(&t.id)
156                    && t.registered_at_ms + u64::from(t.delay_ms) <= target_time
157            });
158
159            let Some(pos) = next else { break };
160            let timer = self.timers.remove(pos);
161            self.simulated_time_ms = timer.registered_at_ms + u64::from(timer.delay_ms);
162
163            if timer.is_interval {
164                self.timers.push(PendingTimer {
165                    id: timer.id,
166                    delay_ms: timer.delay_ms,
167                    is_interval: true,
168                    callback_source: timer.callback_source.clone(),
169                    registered_at_ms: self.simulated_time_ms,
170                });
171            }
172
173            callbacks.push(timer.callback_source);
174        }
175
176        self.simulated_time_ms = target_time;
177        callbacks
178    }
179
180    /// Number of pending (non-cancelled) timers.
181    #[must_use]
182    pub fn pending_count(&self) -> usize {
183        self.timers
184            .iter()
185            .filter(|t| !self.cancelled.contains(&t.id))
186            .count()
187    }
188
189    /// Current simulated time in milliseconds.
190    #[must_use]
191    pub fn simulated_time_ms(&self) -> u64 {
192        self.simulated_time_ms
193    }
194}