Skip to main content

memf_linux/
boot_time.rs

1//! Linux boot time extraction from kernel timekeeper.
2//!
3//! Reads the kernel `timekeeper` struct (via `tk_core` symbol) to derive
4//! the system boot epoch. The wall-clock time at dump capture (`xtime_sec`)
5//! combined with `wall_to_monotonic` and `offs_boot` yields the boot time:
6//!
7//! ```text
8//! boot_epoch = -wall_to_monotonic.tv_sec - (offs_boot / 1_000_000_000)
9//! ```
10//!
11//! This allows converting process `start_time` (nanoseconds since boot)
12//! into absolute wall-clock timestamps for DFIR timelining.
13
14use memf_core::object_reader::ObjectReader;
15use memf_format::PhysicalMemoryProvider;
16
17use crate::{BootTimeEstimate, BootTimeSource, Error, Result};
18
19/// Extract boot time from the kernel timekeeper struct.
20///
21/// Reads `tk_core` (or `timekeeper`) symbol, then extracts:
22/// - `xtime_sec` (wall-clock seconds since Unix epoch at dump time)
23/// - `wall_to_monotonic.tv_sec` (negative offset from wall to monotonic)
24/// - `offs_boot` (nanoseconds spent in suspend, ktime_t/s64)
25///
26/// Returns `boot_epoch = -wall_to_monotonic.tv_sec - offs_boot/1e9`.
27pub fn extract_boot_time<P: PhysicalMemoryProvider>(
28    reader: &ObjectReader<P>,
29) -> Result<BootTimeEstimate> {
30    // Find tk_core symbol (or fall back to timekeeper symbol)
31    let tk_addr = reader
32        .symbols()
33        .symbol_address("tk_core")
34        .or_else(|| reader.symbols().symbol_address("timekeeper"))
35        .ok_or_else(|| Error::MissingKernelSymbol {
36            name: "tk_core".into(),
37        })?;
38
39    // tk_core wraps timekeeper at offset 0 (or is timekeeper itself).
40    // Try reading timekeeper offset within tk_core; if the field doesn't
41    // exist, assume tk_addr IS the timekeeper.
42    let tk_offset = reader
43        .symbols()
44        .field_offset("tk_core", "timekeeper")
45        .unwrap_or(0);
46    let timekeeper_addr = tk_addr + tk_offset;
47
48    // Read xtime_sec (wall-clock at dump time) — validates the timekeeper is readable.
49    let _xtime_sec: i64 = reader.read_field(timekeeper_addr, "timekeeper", "xtime_sec")?;
50
51    // Read wall_to_monotonic (struct timespec64 embedded in timekeeper)
52    let w2m_offset = reader
53        .symbols()
54        .field_offset("timekeeper", "wall_to_monotonic")
55        .ok_or_else(|| Error::MissingField {
56            struct_name: "timekeeper".into(),
57            field_name: "wall_to_monotonic".into(),
58        })?;
59    let w2m_addr = timekeeper_addr + w2m_offset;
60    let w2m_tv_sec: i64 = reader.read_field(w2m_addr, "timespec64", "tv_sec")?;
61
62    // Read offs_boot (ktime_t = s64, nanoseconds in suspend).
63    // May not exist on older kernels — default to 0 (no suspend adjustment).
64    let offs_boot_ns: i64 = reader
65        .read_field(timekeeper_addr, "timekeeper", "offs_boot")
66        .unwrap_or(0);
67
68    // boot_epoch = -wall_to_monotonic.tv_sec - offs_boot_ns / 1_000_000_000
69    let boot_epoch = -w2m_tv_sec - offs_boot_ns / 1_000_000_000;
70
71    Ok(BootTimeEstimate {
72        source: BootTimeSource::Timekeeper,
73        boot_epoch_secs: boot_epoch,
74    })
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::BootTimeSource;
81    use memf_core::object_reader::ObjectReader;
82    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
83    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
84    use memf_symbols::isf::IsfResolver;
85    use memf_symbols::test_builders::IsfBuilder;
86
87    // Synthetic layout:
88    //   tk_core @ symbol address 0xFFFF_8000_0010_0000
89    //     timekeeper @ offset 0 within tk_core (128 bytes)
90    //       xtime_sec     @ 0   (long long, 8 bytes)
91    //       wall_to_monotonic @ 8  (timespec64, 16 bytes)
92    //       offs_boot     @ 24  (long long / ktime_t, 8 bytes)
93    //   timespec64:
94    //     tv_sec  @ 0  (long long, 8 bytes)
95    //     tv_nsec @ 8  (long long, 8 bytes)
96
97    const XTIME_SEC_OFF: usize = 0;
98    const W2M_OFF: usize = 8;
99    const OFFS_BOOT_OFF: usize = 24;
100
101    fn build_boot_time_reader(
102        xtime_sec: i64,
103        w2m_tv_sec: i64,
104        offs_boot_ns: i64,
105    ) -> ObjectReader<SyntheticPhysMem> {
106        let vaddr: u64 = 0xFFFF_8000_0010_0000;
107        let paddr: u64 = 0x0080_0000;
108
109        let isf = IsfBuilder::new()
110            .add_struct("tk_core", 128)
111            .add_field("tk_core", "timekeeper", 0, "timekeeper")
112            .add_struct("timekeeper", 128)
113            .add_field("timekeeper", "xtime_sec", 0, "long long")
114            .add_field("timekeeper", "wall_to_monotonic", 8, "timespec64")
115            .add_field("timekeeper", "offs_boot", 24, "long long")
116            .add_struct("timespec64", 16)
117            .add_field("timespec64", "tv_sec", 0, "long long")
118            .add_field("timespec64", "tv_nsec", 8, "long long")
119            .add_symbol("tk_core", vaddr)
120            .build_json();
121        let resolver = IsfResolver::from_value(&isf).unwrap();
122
123        let mut data = vec![0u8; 4096];
124        data[XTIME_SEC_OFF..XTIME_SEC_OFF + 8].copy_from_slice(&xtime_sec.to_le_bytes());
125        data[W2M_OFF..W2M_OFF + 8].copy_from_slice(&w2m_tv_sec.to_le_bytes());
126        // tv_nsec at W2M_OFF + 8 (leave as 0)
127        data[OFFS_BOOT_OFF..OFFS_BOOT_OFF + 8].copy_from_slice(&offs_boot_ns.to_le_bytes());
128
129        let (cr3, mem) = PageTableBuilder::new()
130            .map_4k(vaddr, paddr, flags::WRITABLE)
131            .write_phys(paddr, &data)
132            .build();
133        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
134        ObjectReader::new(vas, Box::new(resolver))
135    }
136
137    /// System booted at epoch 1_712_000_000, dumped 100_000s later.
138    /// xtime_sec = 1_712_100_000, wall_to_monotonic.tv_sec = -1_712_000_000
139    /// offs_boot = 0 (no suspend).
140    /// Expected boot_epoch = 1_712_000_000.
141    #[test]
142    fn extract_boot_time_no_suspend() {
143        let reader = build_boot_time_reader(
144            1_712_100_000,  // xtime_sec (wall-clock at dump)
145            -1_712_000_000, // wall_to_monotonic.tv_sec
146            0,              // offs_boot (no suspend)
147        );
148        let est = extract_boot_time(&reader).unwrap();
149        assert_eq!(est.source, BootTimeSource::Timekeeper);
150        assert_eq!(est.boot_epoch_secs, 1_712_000_000);
151    }
152
153    /// System was suspended for 7200 seconds (2 hours).
154    /// wall_to_monotonic.tv_sec = -1_712_000_000 (same as no-suspend)
155    /// offs_boot = 7_200_000_000_000 ns (7200s in nanoseconds)
156    /// boot_epoch = -(-1_712_000_000) - 7200 = 1_711_992_800
157    /// (boot was 7200s earlier than monotonic-only would suggest)
158    #[test]
159    fn extract_boot_time_with_suspend() {
160        let reader = build_boot_time_reader(
161            1_712_100_000,
162            -1_712_000_000,
163            7_200_000_000_000, // 7200s in nanoseconds
164        );
165        let est = extract_boot_time(&reader).unwrap();
166        assert_eq!(est.source, BootTimeSource::Timekeeper);
167        assert_eq!(est.boot_epoch_secs, 1_711_992_800);
168    }
169
170    /// Missing tk_core symbol should produce an error.
171    #[test]
172    fn extract_boot_time_missing_symbol() {
173        let isf = IsfBuilder::new()
174            .add_struct("timekeeper", 64)
175            .add_field("timekeeper", "xtime_sec", 0, "long long")
176            .build_json();
177        let resolver = IsfResolver::from_value(&isf).unwrap();
178        let (cr3, mem) = PageTableBuilder::new().build();
179        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
180        let reader = ObjectReader::new(vas, Box::new(resolver));
181
182        let result = extract_boot_time(&reader);
183        assert!(
184            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "tk_core" || name == "timekeeper"),
185            "expected MissingKernelSymbol for tk_core/timekeeper, got {result:?}"
186        );
187    }
188
189    #[test]
190    fn extract_boot_time_missing_wall_to_monotonic_returns_missing_field() {
191        // tk_core symbol present, timekeeper struct present, but wall_to_monotonic missing
192        let tk_vaddr: u64 = 0xFFFF_8000_0010_0000;
193        let tk_paddr: u64 = 0x0080_0000;
194        let mut data = vec![0u8; 4096];
195        // xtime_sec at offset 0 = 1700000000
196        data[0..8].copy_from_slice(&1700000000i64.to_le_bytes());
197
198        let isf = IsfBuilder::new()
199            .add_symbol("tk_core", tk_vaddr)
200            .add_struct("tk_core", 256)
201            .add_field("tk_core", "timekeeper", 0, "timekeeper")
202            .add_struct("timekeeper", 128)
203            .add_field("timekeeper", "xtime_sec", 0, "long long")
204            // wall_to_monotonic intentionally omitted
205            .add_struct("timespec64", 16)
206            .add_field("timespec64", "tv_sec", 0, "long long")
207            .add_field("timespec64", "tv_nsec", 8, "long")
208            .build_json();
209        let resolver = IsfResolver::from_value(&isf).unwrap();
210        let (cr3, mem) = PageTableBuilder::new()
211            .map_4k(tk_vaddr, tk_paddr, flags::WRITABLE)
212            .write_phys(tk_paddr, &data)
213            .build();
214        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
215        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
216        let result = extract_boot_time(&reader);
217        assert!(
218            matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "timekeeper" && field_name == "wall_to_monotonic"),
219            "expected MissingField timekeeper.wall_to_monotonic, got {result:?}"
220        );
221    }
222}