1use memf_core::object_reader::ObjectReader;
12use memf_format::PhysicalMemoryProvider;
13
14use crate::Result;
15
16#[derive(Debug, Clone, serde::Serialize)]
18pub struct KernelTimerInfo {
19 pub address: u64,
21 pub expires: u64,
23 pub function: u64,
25 pub is_periodic: bool,
30 pub is_suspicious: bool,
32}
33
34pub use crate::heuristics::classify_kernel_timer;
40
41const TIMER_WHEEL_GROUPS: usize = 9;
50
51const MAX_TIMERS_PER_VECTOR: usize = 4096;
53
54pub fn walk_kernel_timers<P: PhysicalMemoryProvider>(
59 reader: &ObjectReader<P>,
60) -> Result<Vec<KernelTimerInfo>> {
61 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 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 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 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 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 #[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; 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; 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 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 assert!(
203 !classify_kernel_timer(kernel_start, kernel_start, kernel_end),
204 "function at kernel_start should be benign"
205 );
206
207 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 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 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 .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 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 .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 #[test]
282 fn walk_kernel_timers_symbol_present_all_vectors_zero() {
283 let bases_vaddr: u64 = 0xFFFF_8800_0040_0000;
286 let bases_paddr: u64 = 0x0050_0000;
287
288 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 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 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 .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 .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 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 let bases_vaddr: u64 = 0xFFFF_8800_0060_0000;
357 let bases_paddr: u64 = 0x0060_0000;
358
359 let mut page = [0u8; 4096];
361 let fake_list_addr: u64 = 0xFFFF_DEAD_0000_0000; 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 for i in 0..TIMER_WHEEL_GROUPS {
376 isf_builder =
377 isf_builder.add_field("timer_base", &format!("vectors.{i}"), 0u64, "pointer");
378 }
379 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 assert!(
393 result.is_empty(),
394 "failed walk_list → Err → continue → empty result"
395 );
396 }
397
398 #[test]
404 fn walk_kernel_timers_with_one_timer_in_vector() {
405 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; 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 let suspicious_fn: u64 = 0xFFFF_C900_DEAD_BEEFu64;
438
439 let mut bases_page = [0u8; 4096];
441 bases_page[0..8].copy_from_slice(&listhead_vaddr.to_le_bytes());
442
443 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 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 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 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 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; 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 let mut bases_page = [0u8; 4096];
543 bases_page[0..8].copy_from_slice(&listhead_vaddr.to_le_bytes());
544
545 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 let mut timer_page = [0u8; 4096];
552 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 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 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 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 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 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}