Skip to main content

memf_linux/
kernel_timers.rs

1//! Linux kernel timer enumeration for rootkit callback detection.
2//!
3//! Kernel timers (`timer_list` and `hrtimer`) provide periodic callbacks.
4//! Rootkits use them for periodic check-in, keylogger flushing, or hiding
5//! their tracks. Enumerating kernel timers reveals hidden periodic execution.
6//!
7//! The classifier checks whether a timer callback function address falls
8//! within the kernel text range (`_stext`..`_etext`). Callbacks pointing
9//! outside kernel text are flagged as suspicious.
10
11use memf_core::object_reader::ObjectReader;
12use memf_format::PhysicalMemoryProvider;
13
14use crate::Result;
15
16/// Information about a kernel timer extracted from the timer wheel.
17#[derive(Debug, Clone, serde::Serialize)]
18pub struct KernelTimerInfo {
19    /// Virtual address of the `timer_list` struct.
20    pub address: u64,
21    /// Expiration time in jiffies.
22    pub expires: u64,
23    /// Callback function address.
24    pub function: u64,
25    /// Whether the timer is deferrable / periodic.
26    ///
27    /// `true` when `timer_list.flags & TIMER_DEFERRABLE (0x1)` is non-zero.
28    /// Falls back to `false` if the `flags` field is absent from the ISF profile.
29    pub is_periodic: bool,
30    /// Heuristic flag: callback outside kernel text.
31    pub is_suspicious: bool,
32}
33
34/// Classify a kernel timer callback as suspicious.
35///
36/// - `function == 0` → not suspicious (unset timer, no callback).
37/// - `function` inside `[kernel_start, kernel_end]` → benign (in kernel text).
38/// - `function` outside that range → suspicious (possible rootkit callback).
39pub use crate::heuristics::classify_kernel_timer;
40
41/// Walk kernel timer wheels and enumerate all registered timers.
42///
43/// Looks up the `timer_bases` symbol to find the per-CPU timer base array.
44/// Each timer base contains vectors (timer wheel groups) holding linked lists
45/// of `timer_list` structs. Falls back to `tvec_bases` on older kernels.
46///
47/// Returns `Ok(Vec::new())` if neither symbol is found (graceful degradation).
48/// Number of timer wheel groups (TVR_SIZE buckets per group).
49const TIMER_WHEEL_GROUPS: usize = 9;
50
51/// Maximum number of timers to enumerate per vector (cycle protection).
52const MAX_TIMERS_PER_VECTOR: usize = 4096;
53
54/// Walk the kernel timer wheel and return all active timer entries.
55///
56/// Returns `Ok(Vec::new())` if `timer_bases`, `_stext`, or `_etext` symbols
57/// are absent (graceful degradation for older kernels or incomplete ISF).
58pub fn walk_kernel_timers<P: PhysicalMemoryProvider>(
59    reader: &ObjectReader<P>,
60) -> Result<Vec<KernelTimerInfo>> {
61    // Look up timer_bases (modern kernels) or tvec_bases (older kernels).
62    // Return empty if neither symbol exists (graceful degradation).
63    let timer_bases = reader
64        .symbols()
65        .symbol_address("timer_bases")
66        .or_else(|| reader.symbols().symbol_address("tvec_bases"));
67
68    let Some(bases_addr) = timer_bases else {
69        return Ok(Vec::new());
70    };
71
72    // Resolve kernel text range for classification
73    let Some(kernel_start) = reader.symbols().symbol_address("_stext") else {
74        return Ok(Vec::new());
75    };
76    let Some(kernel_end) = reader.symbols().symbol_address("_etext") else {
77        return Ok(Vec::new());
78    };
79
80    let mut results = Vec::new();
81
82    // Walk timer wheel groups (vectors array within each timer_base)
83    for group in 0..TIMER_WHEEL_GROUPS {
84        let vector_head =
85            match reader.read_pointer(bases_addr, "timer_base", &format!("vectors.{group}")) {
86                Ok(addr) => addr,
87                Err(_) => continue,
88            };
89
90        if vector_head == 0 {
91            continue;
92        }
93
94        // Walk the linked list of timer_list entries in this vector
95        let timer_addrs = match reader.walk_list(vector_head, "timer_list", "entry") {
96            Ok(addrs) => addrs,
97            Err(_) => continue,
98        };
99
100        for (i, &timer_addr) in timer_addrs.iter().enumerate() {
101            if i >= MAX_TIMERS_PER_VECTOR {
102                break;
103            }
104
105            let expires = reader
106                .read_field::<u64>(timer_addr, "timer_list", "expires")
107                .unwrap_or(0);
108
109            let function = reader
110                .read_pointer(timer_addr, "timer_list", "function")
111                .unwrap_or(0);
112
113            let flags = reader
114                .read_field::<u32>(timer_addr, "timer_list", "flags")
115                .unwrap_or(0);
116
117            // TIMER_DEFERRABLE = 0x1 (Linux kernel constant): bit 0 indicates
118            // the timer is deferrable / periodic.
119            let is_periodic = flags & 1 != 0;
120
121            let is_suspicious = classify_kernel_timer(function, kernel_start, kernel_end);
122
123            results.push(KernelTimerInfo {
124                address: timer_addr,
125                expires,
126                function,
127                is_periodic,
128                is_suspicious,
129            });
130        }
131    }
132
133    Ok(results)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use memf_core::test_builders::{flags, PageTableBuilder};
140    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
141    use memf_symbols::isf::IsfResolver;
142    use memf_symbols::test_builders::IsfBuilder;
143
144    // -----------------------------------------------------------------------
145    // classify_kernel_timer tests
146    // -----------------------------------------------------------------------
147
148    #[test]
149    fn classify_kernel_timer_in_kernel_text_is_benign() {
150        let kernel_start = 0xFFFF_8000_0000_0000u64;
151        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
152        let function = kernel_start + 0x1000; // well inside kernel text
153
154        assert!(
155            !classify_kernel_timer(function, kernel_start, kernel_end),
156            "function inside kernel text should not be suspicious"
157        );
158    }
159
160    #[test]
161    fn classify_kernel_timer_outside_kernel_text_is_suspicious() {
162        let kernel_start = 0xFFFF_8000_0000_0000u64;
163        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
164        let function = 0xFFFF_C900_DEAD_BEEFu64; // module space, outside kernel text
165
166        assert!(
167            classify_kernel_timer(function, kernel_start, kernel_end),
168            "function outside kernel text should be suspicious"
169        );
170    }
171
172    #[test]
173    fn classify_kernel_timer_zero_is_not_suspicious() {
174        let kernel_start = 0xFFFF_8000_0000_0000u64;
175        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
176
177        assert!(
178            !classify_kernel_timer(0, kernel_start, kernel_end),
179            "function == 0 (unset timer) should not be suspicious"
180        );
181    }
182
183    #[test]
184    fn classify_kernel_timer_module_space_is_suspicious() {
185        let kernel_start = 0xFFFF_8000_0000_0000u64;
186        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
187        // Typical Linux module space address (above kernel text)
188        let function = 0xFFFF_FFFF_C000_0000u64;
189
190        assert!(
191            classify_kernel_timer(function, kernel_start, kernel_end),
192            "function in module space should be suspicious"
193        );
194    }
195
196    #[test]
197    fn classify_kernel_timer_at_kernel_boundary_is_benign() {
198        let kernel_start = 0xFFFF_8000_0000_0000u64;
199        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
200
201        // Exactly at start boundary
202        assert!(
203            !classify_kernel_timer(kernel_start, kernel_start, kernel_end),
204            "function at kernel_start should be benign"
205        );
206
207        // Exactly at end boundary
208        assert!(
209            !classify_kernel_timer(kernel_end, kernel_start, kernel_end),
210            "function at kernel_end should be benign"
211        );
212    }
213
214    #[test]
215    fn walk_kernel_timers_no_symbol_returns_empty() {
216        // No timer_bases or tvec_bases symbol → should return Ok(empty vec)
217        let isf = IsfBuilder::new()
218            .add_struct("timer_list", 64)
219            .add_field("timer_list", "entry", 0, "list_head")
220            .add_field("timer_list", "expires", 16, "unsigned long")
221            .add_field("timer_list", "function", 24, "pointer")
222            .build_json();
223
224        let resolver = IsfResolver::from_value(&isf).unwrap();
225        let (cr3, mem) = PageTableBuilder::new().build();
226        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
227        let reader = ObjectReader::new(vas, Box::new(resolver));
228
229        let results = walk_kernel_timers(&reader).unwrap();
230        assert!(results.is_empty(), "missing symbol should yield empty vec");
231    }
232
233    #[test]
234    fn walk_kernel_timers_missing_stext_returns_empty() {
235        // timer_bases present but _stext missing → graceful empty
236        let isf = IsfBuilder::new()
237            .add_struct("timer_list", 64)
238            .add_field("timer_list", "entry", 0, "list_head")
239            .add_field("timer_list", "expires", 16, "unsigned long")
240            .add_field("timer_list", "function", 24, "pointer")
241            .add_symbol("timer_bases", 0xFFFF_8000_0010_0000)
242            // _stext intentionally omitted
243            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
244            .build_json();
245
246        let resolver = IsfResolver::from_value(&isf).unwrap();
247        let (cr3, mem) = PageTableBuilder::new().build();
248        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
249        let reader = ObjectReader::new(vas, Box::new(resolver));
250
251        let results = walk_kernel_timers(&reader).unwrap();
252        assert!(results.is_empty(), "missing _stext should yield empty vec");
253    }
254
255    #[test]
256    fn walk_kernel_timers_missing_etext_returns_empty() {
257        // timer_bases + _stext present but _etext missing → graceful empty
258        let isf = IsfBuilder::new()
259            .add_struct("timer_list", 64)
260            .add_field("timer_list", "entry", 0, "list_head")
261            .add_field("timer_list", "expires", 16, "unsigned long")
262            .add_field("timer_list", "function", 24, "pointer")
263            .add_symbol("timer_bases", 0xFFFF_8000_0010_0000)
264            .add_symbol("_stext", 0xFFFF_8000_0000_0000)
265            // _etext intentionally omitted
266            .build_json();
267
268        let resolver = IsfResolver::from_value(&isf).unwrap();
269        let (cr3, mem) = PageTableBuilder::new().build();
270        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
271        let reader = ObjectReader::new(vas, Box::new(resolver));
272
273        let results = walk_kernel_timers(&reader).unwrap();
274        assert!(results.is_empty(), "missing _etext should yield empty vec");
275    }
276
277    // -----------------------------------------------------------------------
278    // walk_kernel_timers: all symbols present, vectors all zero → empty
279    // -----------------------------------------------------------------------
280
281    #[test]
282    fn walk_kernel_timers_symbol_present_all_vectors_zero() {
283        // timer_bases, _stext, _etext all present.
284        // timer_base struct with all vector fields = 0 → each group is skipped.
285        let bases_vaddr: u64 = 0xFFFF_8800_0040_0000;
286        let bases_paddr: u64 = 0x0050_0000;
287
288        // All zeros: every vectors.{n} pointer reads as 0 → continue in loop
289        let page = [0u8; 4096];
290
291        let mut isf_builder = IsfBuilder::new()
292            .add_struct("timer_base", 512)
293            .add_struct("timer_list", 64)
294            .add_field("timer_list", "entry", 0, "pointer")
295            .add_field("timer_list", "expires", 16, "unsigned long")
296            .add_field("timer_list", "function", 24, "pointer")
297            .add_symbol("timer_bases", bases_vaddr)
298            .add_symbol("_stext", 0xFFFF_8000_0000_0000u64)
299            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFFu64);
300
301        // Add vectors.0 .. vectors.8 fields on timer_base (all at offset 0)
302        for i in 0..TIMER_WHEEL_GROUPS {
303            isf_builder =
304                isf_builder.add_field("timer_base", &format!("vectors.{i}"), 0, "pointer");
305        }
306        let isf = isf_builder.build_json();
307
308        let resolver = IsfResolver::from_value(&isf).unwrap();
309        let (cr3, mem) = PageTableBuilder::new()
310            .map_4k(bases_vaddr, bases_paddr, flags::WRITABLE)
311            .write_phys(bases_paddr, &page)
312            .build();
313        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
314        let reader = ObjectReader::new(vas, Box::new(resolver));
315
316        let result = walk_kernel_timers(&reader).unwrap_or_default();
317        assert!(
318            result.is_empty(),
319            "all-zero vector heads should produce no timer entries"
320        );
321    }
322
323    #[test]
324    fn walk_kernel_timers_uses_tvec_bases_fallback() {
325        // timer_bases absent but tvec_bases present → should use tvec_bases
326        // All vectors missing (no timer_base struct) → Err in loop → continue → empty
327        let isf = IsfBuilder::new()
328            .add_struct("timer_list", 64)
329            .add_field("timer_list", "entry", 0, "list_head")
330            .add_field("timer_list", "expires", 16, "unsigned long")
331            .add_field("timer_list", "function", 24, "pointer")
332            // No timer_bases symbol, only tvec_bases
333            .add_symbol("tvec_bases", 0xFFFF_8000_0020_0000u64)
334            .add_symbol("_stext", 0xFFFF_8000_0000_0000u64)
335            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFFu64)
336            // No timer_base struct → read_pointer for vectors.{n} will Err → continue
337            .build_json();
338
339        let resolver = IsfResolver::from_value(&isf).unwrap();
340        let (cr3, mem) = PageTableBuilder::new().build();
341        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
342        let reader = ObjectReader::new(vas, Box::new(resolver));
343
344        // Should not panic; all Err paths → continue → empty result
345        let results = walk_kernel_timers(&reader).unwrap_or_default();
346        assert!(
347            results.is_empty(),
348            "tvec_bases fallback with no vectors → empty"
349        );
350    }
351
352    #[test]
353    fn walk_kernel_timers_vector_nonzero_but_walk_list_fails() {
354        // All three symbols present, timer_base vectors.0 reads as non-zero address,
355        // but walk_list fails because list_head is missing → Err → continue → empty
356        let bases_vaddr: u64 = 0xFFFF_8800_0060_0000;
357        let bases_paddr: u64 = 0x0060_0000;
358
359        // Put a non-zero value at offset 0 so vectors.0 reads as some address
360        let mut page = [0u8; 4096];
361        let fake_list_addr: u64 = 0xFFFF_DEAD_0000_0000; // not mapped
362        page[0..8].copy_from_slice(&fake_list_addr.to_le_bytes());
363
364        let mut isf_builder = IsfBuilder::new()
365            .add_struct("timer_base", 512)
366            .add_struct("timer_list", 64)
367            .add_field("timer_list", "entry", 0, "pointer")
368            .add_field("timer_list", "expires", 16, "unsigned long")
369            .add_field("timer_list", "function", 24, "pointer")
370            .add_symbol("timer_bases", bases_vaddr)
371            .add_symbol("_stext", 0xFFFF_8000_0000_0000u64)
372            .add_symbol("_etext", 0xFFFF_8000_00FF_FFFFu64);
373
374        // vectors.0 at offset 0; rest at same offset (all read the same fake addr)
375        for i in 0..TIMER_WHEEL_GROUPS {
376            isf_builder =
377                isf_builder.add_field("timer_base", &format!("vectors.{i}"), 0u64, "pointer");
378        }
379        // No list_head struct → walk_list will fail → Err → continue
380        let isf = isf_builder.build_json();
381
382        let resolver = IsfResolver::from_value(&isf).unwrap();
383        let (cr3, mem) = PageTableBuilder::new()
384            .map_4k(bases_vaddr, bases_paddr, flags::WRITABLE)
385            .write_phys(bases_paddr, &page)
386            .build();
387        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
388        let reader = ObjectReader::new(vas, Box::new(resolver));
389
390        let result = walk_kernel_timers(&reader).unwrap_or_default();
391        // walk_list fails for unmapped fake_list_addr → continue → empty
392        assert!(
393            result.is_empty(),
394            "failed walk_list → Err → continue → empty result"
395        );
396    }
397
398    // -----------------------------------------------------------------------
399    // walk_kernel_timers: full path — non-zero vector_head + walk_list succeeds
400    // Exercises the loop body (lines 99-120): reads expires, function, classifies.
401    // -----------------------------------------------------------------------
402
403    #[test]
404    fn walk_kernel_timers_with_one_timer_in_vector() {
405        // Layout (all physical addresses < 16 MB):
406        //
407        //   bases_vaddr  / bases_paddr   — timer_base struct; vectors.0 @ offset 0
408        //   list_head_vaddr / list_head_paddr — the list_head sentinel (inode_list_head style)
409        //   timer_vaddr  / timer_paddr   — the timer_list struct
410        //
411        // vector_head = bases_vaddr read via read_pointer(bases_addr, "timer_base", "vectors.0")
412        // walk_list(vector_head, "timer_list", "entry") needs:
413        //   list_head.next offset (list_head struct)
414        //   timer_list.entry offset
415        //
416        // We use a simple one-timer linked list:
417        //   list_head sentinel @ list_head_vaddr: next → timer_vaddr + entry_offset
418        //   timer_list @ timer_vaddr:
419        //     entry (list_head) @ entry_offset: next → list_head_vaddr (wraps back)
420        //     expires @ expires_offset = 9999
421        //     function @ function_offset = some addr OUTSIDE kernel text → suspicious
422
423        let bases_vaddr: u64 = 0xFFFF_8800_00D0_0000;
424        let bases_paddr: u64 = 0x00D0_0000;
425        let listhead_vaddr: u64 = 0xFFFF_8800_00D1_0000;
426        let listhead_paddr: u64 = 0x00D1_0000;
427        let timer_vaddr: u64 = 0xFFFF_8800_00D2_0000;
428        let timer_paddr: u64 = 0x00D2_0000;
429
430        let entry_offset: u64 = 0x00; // timer_list.entry (list_head embedded at start)
431        let expires_offset: u64 = 0x10;
432        let function_offset: u64 = 0x18;
433
434        let kernel_start: u64 = 0xFFFF_8000_0000_0000;
435        let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
436        // A function outside kernel text (module space) → suspicious
437        let suspicious_fn: u64 = 0xFFFF_C900_DEAD_BEEFu64;
438
439        // bases page: vectors.0 at offset 0 = listhead_vaddr
440        let mut bases_page = [0u8; 4096];
441        bases_page[0..8].copy_from_slice(&listhead_vaddr.to_le_bytes());
442
443        // list head sentinel page:
444        //   next @ 0 (list_head.next offset=0) → timer_vaddr + entry_offset
445        let timer_entry_node = timer_vaddr + entry_offset;
446        let mut listhead_page = [0u8; 4096];
447        listhead_page[0..8].copy_from_slice(&timer_entry_node.to_le_bytes());
448
449        // timer_list page:
450        //   entry.next @ 0 → listhead_vaddr   (next iteration hits head → walk ends)
451        //   expires    @ expires_offset = 9999
452        //   function   @ function_offset = suspicious_fn
453        let mut timer_page = [0u8; 4096];
454        timer_page[entry_offset as usize..entry_offset as usize + 8]
455            .copy_from_slice(&listhead_vaddr.to_le_bytes());
456        timer_page[expires_offset as usize..expires_offset as usize + 8]
457            .copy_from_slice(&9999u64.to_le_bytes());
458        timer_page[function_offset as usize..function_offset as usize + 8]
459            .copy_from_slice(&suspicious_fn.to_le_bytes());
460
461        let mut isf_builder = IsfBuilder::new()
462            .add_struct("list_head", 0x10)
463            .add_field("list_head", "next", 0x00u64, "pointer")
464            .add_struct("timer_base", 512)
465            .add_struct("timer_list", 64)
466            .add_field("timer_list", "entry", entry_offset, "pointer")
467            .add_field("timer_list", "expires", expires_offset, "unsigned long")
468            .add_field("timer_list", "function", function_offset, "pointer")
469            .add_symbol("timer_bases", bases_vaddr)
470            .add_symbol("_stext", kernel_start)
471            .add_symbol("_etext", kernel_end);
472
473        for i in 0..TIMER_WHEEL_GROUPS {
474            // All vectors point to offset 0 of bases_page (listhead_vaddr).
475            // That means every group finds the same single timer, but walk_list
476            // will succeed for all groups. For simplicity, only vectors.0 at offset 0
477            // actually has a non-zero value; the rest are at offset 0 too but that's fine —
478            // they'll all point to listhead_vaddr and each find the one timer.
479            // To avoid inflating the assertion, let's only wire vectors.0 to listhead_vaddr
480            // and place the remaining vector fields at a different offset (so they read 0).
481            let field_offset: u64 = if i == 0 { 0 } else { 8 + i as u64 * 8 };
482            isf_builder = isf_builder.add_field(
483                "timer_base",
484                &format!("vectors.{i}"),
485                field_offset,
486                "pointer",
487            );
488        }
489        let isf = isf_builder.build_json();
490
491        let resolver = IsfResolver::from_value(&isf).unwrap();
492        let (cr3, mem) = PageTableBuilder::new()
493            .map_4k(bases_vaddr, bases_paddr, flags::WRITABLE)
494            .write_phys(bases_paddr, &bases_page)
495            .map_4k(listhead_vaddr, listhead_paddr, flags::WRITABLE)
496            .write_phys(listhead_paddr, &listhead_page)
497            .map_4k(timer_vaddr, timer_paddr, flags::WRITABLE)
498            .write_phys(timer_paddr, &timer_page)
499            .build();
500
501        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
502        let reader = ObjectReader::new(vas, Box::new(resolver));
503
504        let result = walk_kernel_timers(&reader).unwrap();
505        // vectors.0 yields one timer; remaining vector offsets read 0 → skipped
506        assert!(!result.is_empty(), "should find at least one timer");
507        let timer = &result[0];
508        assert_eq!(timer.expires, 9999);
509        assert_eq!(timer.function, suspicious_fn);
510        assert!(
511            timer.is_suspicious,
512            "function outside kernel text must be suspicious"
513        );
514    }
515
516    // -----------------------------------------------------------------------
517    // is_periodic: TIMER_DEFERRABLE flag (bit 0) tests
518    // -----------------------------------------------------------------------
519
520    /// Build a full single-timer walk setup and return the first timer result.
521    ///
522    /// `flags_value` is written into the timer_list.flags field.
523    /// The ISF includes a `flags` field on `timer_list` at `flags_offset`.
524    fn walk_one_timer_with_flags(flags_value: u32) -> KernelTimerInfo {
525        let bases_vaddr: u64 = 0xFFFF_8800_00E0_0000;
526        let bases_paddr: u64 = 0x00E0_0000;
527        let listhead_vaddr: u64 = 0xFFFF_8800_00E1_0000;
528        let listhead_paddr: u64 = 0x00E1_0000;
529        let timer_vaddr: u64 = 0xFFFF_8800_00E2_0000;
530        let timer_paddr: u64 = 0x00E2_0000;
531
532        let entry_offset: u64 = 0x00;
533        let expires_offset: u64 = 0x10;
534        let function_offset: u64 = 0x18;
535        let flags_offset: u64 = 0x20; // u32 field after function pointer
536
537        let kernel_start: u64 = 0xFFFF_8000_0000_0000;
538        let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
539        let benign_fn: u64 = kernel_start + 0x1000;
540
541        // bases page: vectors.0 at offset 0 = listhead_vaddr
542        let mut bases_page = [0u8; 4096];
543        bases_page[0..8].copy_from_slice(&listhead_vaddr.to_le_bytes());
544
545        // list head sentinel: next → timer entry node
546        let timer_entry_node = timer_vaddr + entry_offset;
547        let mut listhead_page = [0u8; 4096];
548        listhead_page[0..8].copy_from_slice(&timer_entry_node.to_le_bytes());
549
550        // timer_list page
551        let mut timer_page = [0u8; 4096];
552        // entry.next → listhead_vaddr (terminate walk)
553        timer_page[entry_offset as usize..entry_offset as usize + 8]
554            .copy_from_slice(&listhead_vaddr.to_le_bytes());
555        timer_page[expires_offset as usize..expires_offset as usize + 8]
556            .copy_from_slice(&1234u64.to_le_bytes());
557        timer_page[function_offset as usize..function_offset as usize + 8]
558            .copy_from_slice(&benign_fn.to_le_bytes());
559        timer_page[flags_offset as usize..flags_offset as usize + 4]
560            .copy_from_slice(&flags_value.to_le_bytes());
561
562        let mut isf_builder = IsfBuilder::new()
563            .add_struct("list_head", 0x10)
564            .add_field("list_head", "next", 0x00u64, "pointer")
565            .add_struct("timer_base", 512)
566            .add_struct("timer_list", 64)
567            .add_field("timer_list", "entry", entry_offset, "pointer")
568            .add_field("timer_list", "expires", expires_offset, "unsigned long")
569            .add_field("timer_list", "function", function_offset, "pointer")
570            .add_field("timer_list", "flags", flags_offset, "unsigned int")
571            .add_symbol("timer_bases", bases_vaddr)
572            .add_symbol("_stext", kernel_start)
573            .add_symbol("_etext", kernel_end);
574
575        for i in 0..TIMER_WHEEL_GROUPS {
576            let field_offset: u64 = if i == 0 { 0 } else { 8 + i as u64 * 8 };
577            isf_builder = isf_builder.add_field(
578                "timer_base",
579                &format!("vectors.{i}"),
580                field_offset,
581                "pointer",
582            );
583        }
584        let isf = isf_builder.build_json();
585
586        let resolver = IsfResolver::from_value(&isf).unwrap();
587        let (cr3, mem) = PageTableBuilder::new()
588            .map_4k(bases_vaddr, bases_paddr, flags::WRITABLE)
589            .write_phys(bases_paddr, &bases_page)
590            .map_4k(listhead_vaddr, listhead_paddr, flags::WRITABLE)
591            .write_phys(listhead_paddr, &listhead_page)
592            .map_4k(timer_vaddr, timer_paddr, flags::WRITABLE)
593            .write_phys(timer_paddr, &timer_page)
594            .build();
595
596        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
597        let reader = ObjectReader::new(vas, Box::new(resolver));
598
599        let mut result = walk_kernel_timers(&reader).unwrap();
600        assert!(!result.is_empty(), "expected at least one timer");
601        result.remove(0)
602    }
603
604    #[test]
605    fn walk_kernel_timers_is_periodic_true_when_deferrable_bit_set() {
606        // TIMER_DEFERRABLE = 0x1 — bit 0 set → is_periodic must be true
607        let timer = walk_one_timer_with_flags(0x1);
608        assert!(
609            timer.is_periodic,
610            "flags & 1 != 0 (TIMER_DEFERRABLE) must set is_periodic = true"
611        );
612    }
613
614    #[test]
615    fn walk_kernel_timers_is_periodic_false_when_flags_zero() {
616        // flags == 0 → no deferrable bit → is_periodic must be false
617        let timer = walk_one_timer_with_flags(0x0);
618        assert!(
619            !timer.is_periodic,
620            "flags == 0 must leave is_periodic = false"
621        );
622    }
623
624    #[test]
625    fn walk_kernel_timers_is_periodic_true_when_other_bits_plus_deferrable() {
626        // flags = 0x5 (bit 0 + bit 2) → bit 0 set → is_periodic true
627        let timer = walk_one_timer_with_flags(0x5);
628        assert!(
629            timer.is_periodic,
630            "flags = 0x5 has bit 0 set → is_periodic = true"
631        );
632    }
633
634    #[test]
635    fn walk_kernel_timers_is_periodic_false_when_only_non_deferrable_bits_set() {
636        // flags = 0x4 (TIMER_PINNED only, bit 0 clear) → is_periodic false
637        let timer = walk_one_timer_with_flags(0x4);
638        assert!(
639            !timer.is_periodic,
640            "flags = 0x4 (bit 0 clear) → is_periodic = false"
641        );
642    }
643
644    #[test]
645    fn classify_kernel_timer_just_below_kernel_start_is_suspicious() {
646        let kernel_start = 0xFFFF_8000_0000_0000u64;
647        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
648        // One below kernel_start but non-zero
649        let function = kernel_start - 1;
650        assert!(
651            classify_kernel_timer(function, kernel_start, kernel_end),
652            "function just below kernel_start should be suspicious"
653        );
654    }
655
656    #[test]
657    fn classify_kernel_timer_just_above_kernel_end_is_suspicious() {
658        let kernel_start = 0xFFFF_8000_0000_0000u64;
659        let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
660        let function = kernel_end + 1;
661        assert!(
662            classify_kernel_timer(function, kernel_start, kernel_end),
663            "function just above kernel_end should be suspicious"
664        );
665    }
666}