1use memf_core::object_reader::ObjectReader;
7use memf_format::PhysicalMemoryProvider;
8
9use crate::{vma_walker::for_each_task_vma, CrontabEntry, Error, Result};
10
11const CRON_PROCS: &[&str] = &["cron", "crond", "anacron", "atd"];
13
14const MAX_REGION_SCAN: u64 = 4 * 1024 * 1024;
16
17pub fn walk_crontab_entries<P: PhysicalMemoryProvider>(
23 reader: &ObjectReader<P>,
24) -> Result<Vec<CrontabEntry>> {
25 let init_task_addr = reader
26 .symbols()
27 .symbol_address("init_task")
28 .ok_or_else(|| Error::MissingKernelSymbol {
29 name: "init_task".into(),
30 })?;
31
32 let tasks_offset = reader
33 .symbols()
34 .field_offset("task_struct", "tasks")
35 .ok_or_else(|| Error::MissingField {
36 struct_name: "task_struct".into(),
37 field_name: "tasks".into(),
38 })?;
39
40 let head_vaddr = init_task_addr + tasks_offset;
41 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
42
43 let mut results = Vec::new();
44
45 scan_process_crontab(reader, init_task_addr, &mut results);
47
48 for &task_addr in &task_addrs {
49 scan_process_crontab(reader, task_addr, &mut results);
50 }
51
52 Ok(results)
53}
54
55fn scan_process_crontab<P: PhysicalMemoryProvider>(
57 reader: &ObjectReader<P>,
58 task_addr: u64,
59 out: &mut Vec<CrontabEntry>,
60) {
61 let Ok(comm) = reader.read_field_string(task_addr, "task_struct", "comm", 16) else {
62 return;
63 };
64
65 if !CRON_PROCS.iter().any(|name| comm == *name) {
66 return;
67 }
68
69 let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
70 Ok(v) => v,
71 Err(_) => return,
72 };
73
74 let mut readable_regions: Vec<(u64, u64)> = Vec::new();
76
77 for_each_task_vma(reader, task_addr, &mut |e| {
78 if e.flags.read {
79 readable_regions.push((e.start, e.end));
80 }
81 });
82
83 for &(start, end) in &readable_regions {
85 let size = end.saturating_sub(start);
86 if size == 0 || size > MAX_REGION_SCAN {
87 continue;
88 }
89 let Ok(data) = reader.read_bytes(start, size as usize) else {
90 continue;
91 };
92 for chunk in data.split(|&b| b == 0) {
95 if chunk.is_empty() {
96 continue;
97 }
98 let text = String::from_utf8_lossy(chunk);
99 for line in text.lines() {
100 let trimmed = line.trim();
101 if is_crontab_line(trimmed) {
102 out.push(CrontabEntry {
103 pid: u64::from(pid),
104 comm: comm.clone(),
105 line: trimmed.to_string(),
106 });
107 }
108 }
109 }
110 }
111}
112
113fn is_crontab_line(line: &str) -> bool {
118 if line.is_empty() || line.starts_with('#') {
120 return false;
121 }
122 if let Some(eq_pos) = line.find('=') {
124 if !line[..eq_pos].contains(' ') {
125 return false;
126 }
127 }
128
129 let parts: Vec<&str> = line.split_whitespace().collect();
130 if parts.len() < 6 {
131 return false;
132 }
133
134 for field in &parts[..5] {
136 if !is_cron_time_field(field) {
137 return false;
138 }
139 }
140
141 let cmd = parts[5];
143 cmd.starts_with('/') || cmd.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
144}
145
146fn is_cron_time_field(field: &str) -> bool {
150 if field == "*" {
151 return true;
152 }
153 !field.is_empty()
154 && field
155 .chars()
156 .all(|c| c.is_ascii_digit() || c == '*' || c == '/' || c == '-' || c == ',')
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn is_crontab_line_valid() {
165 assert!(is_crontab_line("0 * * * * /usr/bin/backup.sh"));
166 assert!(is_crontab_line("*/5 * * * * curl http://example.com"));
167 assert!(is_crontab_line("0 0 1 * * /bin/monthly_report"));
168 assert!(is_crontab_line("30 2 * * 1-5 /opt/weekday_job"));
169 }
170
171 #[test]
172 fn is_crontab_line_invalid() {
173 assert!(!is_crontab_line(""));
174 assert!(!is_crontab_line("# This is a comment"));
175 assert!(!is_crontab_line("PATH=/usr/bin"));
176 assert!(!is_crontab_line("hello world"));
177 assert!(!is_crontab_line("abc def ghi jkl mno pqr")); }
179
180 #[test]
181 fn is_cron_time_field_valid() {
182 assert!(is_cron_time_field("*"));
183 assert!(is_cron_time_field("0"));
184 assert!(is_cron_time_field("*/5"));
185 assert!(is_cron_time_field("1-5"));
186 assert!(is_cron_time_field("0,15,30,45"));
187 }
188
189 #[test]
190 fn is_cron_time_field_invalid() {
191 assert!(!is_cron_time_field(""));
192 assert!(!is_cron_time_field("abc"));
193 assert!(!is_cron_time_field("hello"));
194 }
195
196 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
199 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
200 use memf_symbols::isf::IsfResolver;
201 use memf_symbols::test_builders::IsfBuilder;
202
203 fn make_test_reader(
204 data: &[u8],
205 vaddr: u64,
206 paddr: u64,
207 extra_mappings: &[(u64, u64, &[u8])],
208 ) -> ObjectReader<SyntheticPhysMem> {
209 let isf = IsfBuilder::new()
210 .add_struct("task_struct", 128)
211 .add_field("task_struct", "pid", 0, "int")
212 .add_field("task_struct", "state", 4, "long")
213 .add_field("task_struct", "tasks", 16, "list_head")
214 .add_field("task_struct", "comm", 32, "char")
215 .add_field("task_struct", "mm", 48, "pointer")
216 .add_struct("list_head", 16)
217 .add_field("list_head", "next", 0, "pointer")
218 .add_field("list_head", "prev", 8, "pointer")
219 .add_struct("mm_struct", 128)
220 .add_field("mm_struct", "pgd", 0, "pointer")
221 .add_field("mm_struct", "mmap", 8, "pointer")
222 .add_struct("vm_area_struct", 64)
223 .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
224 .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
225 .add_field("vm_area_struct", "vm_next", 16, "pointer")
226 .add_field("vm_area_struct", "vm_flags", 24, "unsigned long")
227 .add_field("vm_area_struct", "vm_pgoff", 32, "unsigned long")
228 .add_field("vm_area_struct", "vm_file", 40, "pointer")
229 .add_symbol("init_task", vaddr)
230 .build_json();
231
232 let resolver = IsfResolver::from_value(&isf).unwrap();
233 let mut builder = PageTableBuilder::new()
234 .map_4k(vaddr, paddr, ptflags::WRITABLE)
235 .write_phys(paddr, data);
236
237 for &(ev, ep, edata) in extra_mappings {
238 builder = builder
239 .map_4k(ev, ep, ptflags::WRITABLE)
240 .write_phys(ep, edata);
241 }
242
243 let (cr3, mem) = builder.build();
244 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
245 ObjectReader::new(vas, Box::new(resolver))
246 }
247
248 fn build_heap_with_crontab(entries: &[&str]) -> Vec<u8> {
250 let mut heap = vec![0u8; 4096];
251 let text = entries.join("\n");
252 let bytes = text.as_bytes();
253 let len = bytes.len().min(4096);
254 heap[..len].copy_from_slice(&bytes[..len]);
255 heap
256 }
257
258 #[test]
260 fn recovers_crontab_from_crond_heap() {
261 let vaddr: u64 = 0xFFFF_8000_0010_0000;
262 let paddr: u64 = 0x0080_0000;
263 let mut data = vec![0u8; 4096];
264
265 data[0..4].copy_from_slice(&100u32.to_le_bytes());
267 let tasks_addr = vaddr + 16;
268 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes()); data[24..32].copy_from_slice(&tasks_addr.to_le_bytes()); data[32..37].copy_from_slice(b"crond");
271 let mm_addr = vaddr + 0x200;
272 data[48..56].copy_from_slice(&mm_addr.to_le_bytes()); data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes()); let vma_addr = vaddr + 0x300;
277 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes()); let heap_vaddr: u64 = 0x0000_5555_0000_0000;
281 let heap_paddr: u64 = 0x0090_0000;
282 data[0x300..0x308].copy_from_slice(&heap_vaddr.to_le_bytes()); data[0x308..0x310].copy_from_slice(&(heap_vaddr + 0x1000).to_le_bytes()); data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); data[0x318..0x320].copy_from_slice(&0x1u64.to_le_bytes()); data[0x320..0x328].copy_from_slice(&0u64.to_le_bytes()); data[0x328..0x330].copy_from_slice(&0u64.to_le_bytes()); let heap = build_heap_with_crontab(&[
290 "0 * * * * /usr/bin/backup.sh",
291 "*/5 * * * * curl http://example.com",
292 "# this is a comment",
293 "30 2 * * 1-5 /opt/weekday_job",
294 ]);
295
296 let reader = make_test_reader(&data, vaddr, paddr, &[(heap_vaddr, heap_paddr, &heap)]);
297 let results = walk_crontab_entries(&reader).unwrap();
298
299 assert_eq!(results.len(), 3);
300 assert_eq!(results[0].pid, 100);
301 assert_eq!(results[0].comm, "crond");
302 assert_eq!(results[0].line, "0 * * * * /usr/bin/backup.sh");
303 assert_eq!(results[1].line, "*/5 * * * * curl http://example.com");
304 assert_eq!(results[2].line, "30 2 * * 1-5 /opt/weekday_job");
305 }
306
307 #[test]
308 fn skips_non_cron_processes() {
309 let vaddr: u64 = 0xFFFF_8000_0010_0000;
310 let paddr: u64 = 0x0080_0000;
311 let mut data = vec![0u8; 4096];
312
313 data[0..4].copy_from_slice(&1u32.to_le_bytes());
315 let tasks_addr = vaddr + 16;
316 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
317 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
318 data[32..37].copy_from_slice(b"nginx");
319 let mm_addr = vaddr + 0x200;
320 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
321
322 data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes());
323 data[0x208..0x210].copy_from_slice(&0u64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr, &[]);
326 let results = walk_crontab_entries(&reader).unwrap();
327
328 assert!(results.is_empty());
329 }
330
331 #[test]
333 fn skips_kernel_threads() {
334 let vaddr: u64 = 0xFFFF_8000_0010_0000;
335 let paddr: u64 = 0x0080_0000;
336 let mut data = vec![0u8; 4096];
337
338 data[0..4].copy_from_slice(&0u32.to_le_bytes());
340 let tasks_addr = vaddr + 16;
341 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
342 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
343 data[32..36].copy_from_slice(b"cron");
344 data[48..56].copy_from_slice(&0u64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr, &[]);
347 let results = walk_crontab_entries(&reader).unwrap();
348
349 assert!(results.is_empty());
350 }
351
352 #[test]
353 fn missing_init_task_symbol() {
354 let isf = IsfBuilder::new()
355 .add_struct("task_struct", 64)
356 .add_field("task_struct", "pid", 0, "int")
357 .add_field("task_struct", "tasks", 8, "list_head")
358 .add_struct("list_head", 16)
359 .add_field("list_head", "next", 0, "pointer")
360 .add_field("list_head", "prev", 8, "pointer")
361 .build_json();
362
363 let resolver = IsfResolver::from_value(&isf).unwrap();
364 let (cr3, mem) = PageTableBuilder::new().build();
365 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
366 let reader = ObjectReader::new(vas, Box::new(resolver));
367
368 let result = walk_crontab_entries(&reader);
369 assert!(
370 matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "init_task"),
371 "expected MissingKernelSymbol {{name: \"init_task\"}}, got {result:?}"
372 );
373 }
374
375 #[test]
376 fn missing_tasks_field_returns_missing_field() {
377 let isf = IsfBuilder::new()
378 .add_struct("task_struct", 64)
379 .add_field("task_struct", "pid", 0, "int")
380 .add_struct("list_head", 16)
382 .add_field("list_head", "next", 0, "pointer")
383 .add_field("list_head", "prev", 8, "pointer")
384 .add_symbol("init_task", 0xFFFF_8000_0010_0000)
385 .build_json();
386
387 let resolver = IsfResolver::from_value(&isf).unwrap();
388 let (cr3, mem) = PageTableBuilder::new().build();
389 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
390 let reader = ObjectReader::new(vas, Box::new(resolver));
391
392 let result = walk_crontab_entries(&reader);
393 assert!(
394 matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "task_struct" && field_name == "tasks"),
395 "expected MissingField task_struct.tasks, got {result:?}"
396 );
397 }
398
399 #[test]
400 fn recognizes_all_cron_daemon_names() {
401 assert!(CRON_PROCS.contains(&"cron"));
403 assert!(CRON_PROCS.contains(&"crond"));
404 assert!(CRON_PROCS.contains(&"anacron"));
405 assert!(CRON_PROCS.contains(&"atd"));
406 assert!(!CRON_PROCS.contains(&"bash"));
407 }
408}