Skip to main content

memf_linux/
ssh_keys.rs

1//! SSH key extraction from sshd process memory.
2//!
3//! Scans sshd process heap and mapped memory for SSH public key material
4//! (e.g. `ssh-rsa`, `ssh-ed25519`). During incident response this reveals
5//! lateral movement paths and compromised credentials by recovering keys
6//! that were present in the SSH daemon's address space at the time of
7//! the memory capture.
8
9use memf_core::object_reader::ObjectReader;
10use memf_format::PhysicalMemoryProvider;
11
12use crate::{Error, Result, SshKeyInfo, SshKeyType, VmaFlags};
13
14/// SSH key type prefixes to scan for.
15const SSH_KEY_PREFIXES: &[(&str, SshKeyType)] = &[
16    ("ssh-rsa ", SshKeyType::Rsa),
17    ("ssh-ed25519 ", SshKeyType::Ed25519),
18    ("ssh-dss ", SshKeyType::Dsa),
19    ("ecdsa-sha2-nistp256 ", SshKeyType::Ecdsa256),
20    ("ecdsa-sha2-nistp384 ", SshKeyType::Ecdsa384),
21    ("ecdsa-sha2-nistp521 ", SshKeyType::Ecdsa521),
22];
23
24/// Maximum key line length (bytes) before we stop reading.
25const MAX_KEY_LINE: usize = 8192;
26
27/// Maximum VMA region size to scan (16 MiB safety limit).
28const MAX_VMA_SCAN: u64 = 16 * 1024 * 1024;
29
30/// Extract SSH public keys from sshd process memory.
31///
32/// Walks the process list to find `sshd` processes, then scans their
33/// readable VMAs for SSH key prefix strings. When a prefix is found,
34/// extracts the full key line (up to newline/null, max 8 KiB) and
35/// parses the key type, base64 data, and optional comment.
36///
37/// Results are deduplicated by `(pid, key_data)`.
38pub fn extract_ssh_keys<P: PhysicalMemoryProvider>(
39    reader: &ObjectReader<P>,
40) -> Result<Vec<SshKeyInfo>> {
41    let init_task_addr = reader
42        .symbols()
43        .symbol_address("init_task")
44        .ok_or_else(|| Error::MissingKernelSymbol {
45            name: "init_task".into(),
46        })?;
47
48    let tasks_offset = reader
49        .symbols()
50        .field_offset("task_struct", "tasks")
51        .ok_or_else(|| Error::MissingField {
52            struct_name: "task_struct".into(),
53            field_name: "tasks".into(),
54        })?;
55
56    let head_vaddr = init_task_addr + tasks_offset;
57    let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
58
59    let mut results = Vec::new();
60    let mut seen = std::collections::HashSet::new();
61
62    // Scan init_task itself
63    scan_sshd_keys(reader, init_task_addr, &mut results, &mut seen);
64
65    for &task_addr in &task_addrs {
66        scan_sshd_keys(reader, task_addr, &mut results, &mut seen);
67    }
68
69    Ok(results)
70}
71
72/// Check if a task is sshd and, if so, scan its VMAs for SSH keys.
73fn scan_sshd_keys<P: PhysicalMemoryProvider>(
74    reader: &ObjectReader<P>,
75    task_addr: u64,
76    results: &mut Vec<SshKeyInfo>,
77    seen: &mut std::collections::HashSet<(u64, String)>,
78) {
79    let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
80        Ok(v) => v,
81        Err(_) => return,
82    };
83    let Ok(comm) = reader.read_field_string(task_addr, "task_struct", "comm", 16) else {
84        return;
85    };
86
87    if comm != "sshd" {
88        return;
89    }
90
91    let mm_ptr: u64 = match reader.read_field(task_addr, "task_struct", "mm") {
92        Ok(v) => v,
93        Err(_) => return,
94    };
95    if mm_ptr == 0 {
96        return; // kernel thread
97    }
98
99    let mmap_ptr: u64 = match reader.read_field(mm_ptr, "mm_struct", "mmap") {
100        Ok(v) => v,
101        Err(_) => return,
102    };
103
104    // Walk VMAs
105    let mut vma_addr = mmap_ptr;
106    let mut vma_count = 0u32;
107    while vma_addr != 0 && vma_count < 4096 {
108        vma_count += 1;
109
110        let vm_start: u64 = match reader.read_field(vma_addr, "vm_area_struct", "vm_start") {
111            Ok(v) => v,
112            Err(_) => break,
113        };
114        let vm_end: u64 = match reader.read_field(vma_addr, "vm_area_struct", "vm_end") {
115            Ok(v) => v,
116            Err(_) => break,
117        };
118        let vm_flags: u64 = match reader.read_field(vma_addr, "vm_area_struct", "vm_flags") {
119            Ok(v) => v,
120            Err(_) => break,
121        };
122
123        let flags = VmaFlags::from_raw(vm_flags);
124        let size = vm_end.saturating_sub(vm_start);
125
126        // Only scan readable regions within size limit
127        if flags.read && size > 0 && size <= MAX_VMA_SCAN {
128            scan_region_for_keys(reader, u64::from(pid), vm_start, size, results, seen);
129        }
130
131        // Follow vm_next
132        vma_addr = match reader.read_field(vma_addr, "vm_area_struct", "vm_next") {
133            Ok(v) => v,
134            Err(_) => break,
135        };
136    }
137}
138
139/// Scan a memory region for SSH key prefixes.
140fn scan_region_for_keys<P: PhysicalMemoryProvider>(
141    reader: &ObjectReader<P>,
142    pid: u64,
143    start: u64,
144    size: u64,
145    results: &mut Vec<SshKeyInfo>,
146    seen: &mut std::collections::HashSet<(u64, String)>,
147) {
148    let Ok(buf) = reader.read_bytes(start, size as usize) else {
149        return;
150    };
151
152    for &(prefix, _key_type) in SSH_KEY_PREFIXES {
153        let prefix_bytes = prefix.as_bytes();
154        // Scan for all occurrences of this prefix in the buffer
155        let mut search_from = 0;
156        while search_from + prefix_bytes.len() <= buf.len() {
157            let haystack = &buf[search_from..];
158            let Some(pos) = find_bytes(haystack, prefix_bytes) else {
159                break;
160            };
161
162            let abs_pos = search_from + pos;
163
164            // Extract key line: from prefix position to newline/null/end, max MAX_KEY_LINE
165            let line_start = abs_pos;
166            let max_end = buf.len().min(line_start + MAX_KEY_LINE);
167            let line_end = buf[line_start..max_end]
168                .iter()
169                .position(|&b| b == b'\n' || b == b'\0' || b == b'\r')
170                .map_or(max_end, |p| line_start + p);
171
172            let line_bytes = &buf[line_start..line_end];
173            if let Ok(line_str) = std::str::from_utf8(line_bytes) {
174                if let Some((key_type, key_data, comment)) = parse_key_line(line_str) {
175                    let dedup_key = (pid, key_data.clone());
176                    if seen.insert(dedup_key) {
177                        results.push(SshKeyInfo {
178                            pid,
179                            key_type,
180                            key_data,
181                            comment,
182                        });
183                    }
184                }
185            }
186
187            // Advance past this match
188            search_from = abs_pos + prefix_bytes.len();
189        }
190    }
191}
192
193/// Find the first occurrence of `needle` in `haystack`.
194fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
195    haystack.windows(needle.len()).position(|w| w == needle)
196}
197
198/// Parse a key line into `(key_type, full_key_data, comment)`.
199///
200/// The key line format is: `<type> <base64> [comment]`
201fn parse_key_line(line: &str) -> Option<(SshKeyType, String, String)> {
202    let trimmed = line.trim();
203    if trimmed.is_empty() {
204        return None;
205    }
206
207    // Find the type prefix
208    let space_idx = trimmed.find(' ')?;
209    let type_str = &trimmed[..space_idx];
210    let key_type = SshKeyType::from_prefix(type_str);
211    if key_type == SshKeyType::Unknown {
212        return None;
213    }
214
215    let rest = &trimmed[space_idx + 1..];
216
217    // Split on the next space to get base64 and optional comment
218    let (base64_data, comment) = match rest.find(' ') {
219        Some(idx) => (&rest[..idx], rest[idx + 1..].trim()),
220        None => (rest, ""),
221    };
222
223    // Sanity: base64 data should be non-empty and look like base64
224    if base64_data.is_empty() {
225        return None;
226    }
227
228    let full_key = format!("{type_str} {base64_data}");
229    Some((key_type, full_key, comment.to_string()))
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
236    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
237    use memf_symbols::isf::IsfResolver;
238    use memf_symbols::test_builders::IsfBuilder;
239
240    fn make_test_reader(
241        data: &[u8],
242        vaddr: u64,
243        paddr: u64,
244        extra_mappings: &[(u64, u64, &[u8])],
245    ) -> ObjectReader<SyntheticPhysMem> {
246        let isf = IsfBuilder::new()
247            .add_struct("task_struct", 128)
248            .add_field("task_struct", "pid", 0, "int")
249            .add_field("task_struct", "state", 4, "long")
250            .add_field("task_struct", "tasks", 16, "list_head")
251            .add_field("task_struct", "comm", 32, "char")
252            .add_field("task_struct", "mm", 48, "pointer")
253            .add_struct("list_head", 16)
254            .add_field("list_head", "next", 0, "pointer")
255            .add_field("list_head", "prev", 8, "pointer")
256            .add_struct("mm_struct", 128)
257            .add_field("mm_struct", "pgd", 0, "pointer")
258            .add_field("mm_struct", "mmap", 8, "pointer")
259            .add_struct("vm_area_struct", 64)
260            .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
261            .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
262            .add_field("vm_area_struct", "vm_next", 16, "pointer")
263            .add_field("vm_area_struct", "vm_flags", 24, "unsigned long")
264            .add_field("vm_area_struct", "vm_pgoff", 32, "unsigned long")
265            .add_field("vm_area_struct", "vm_file", 40, "pointer")
266            .add_symbol("init_task", vaddr)
267            .build_json();
268
269        let resolver = IsfResolver::from_value(&isf).unwrap();
270        let mut builder = PageTableBuilder::new()
271            .map_4k(vaddr, paddr, ptflags::WRITABLE)
272            .write_phys(paddr, data);
273
274        for &(ev, ep, edata) in extra_mappings {
275            builder = builder
276                .map_4k(ev, ep, ptflags::WRITABLE)
277                .write_phys(ep, edata);
278        }
279
280        let (cr3, mem) = builder.build();
281        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
282        ObjectReader::new(vas, Box::new(resolver))
283    }
284
285    #[test]
286    fn ssh_key_type_from_prefix() {
287        assert_eq!(SshKeyType::from_prefix("ssh-rsa"), SshKeyType::Rsa);
288        assert_eq!(SshKeyType::from_prefix("ssh-ed25519"), SshKeyType::Ed25519);
289        assert_eq!(SshKeyType::from_prefix("ssh-dss"), SshKeyType::Dsa);
290        assert_eq!(
291            SshKeyType::from_prefix("ecdsa-sha2-nistp256"),
292            SshKeyType::Ecdsa256
293        );
294        assert_eq!(
295            SshKeyType::from_prefix("ecdsa-sha2-nistp384"),
296            SshKeyType::Ecdsa384
297        );
298        assert_eq!(
299            SshKeyType::from_prefix("ecdsa-sha2-nistp521"),
300            SshKeyType::Ecdsa521
301        );
302        assert_eq!(SshKeyType::from_prefix("bogus"), SshKeyType::Unknown);
303        assert_eq!(SshKeyType::from_prefix(""), SshKeyType::Unknown);
304    }
305
306    #[test]
307    fn ssh_key_type_display() {
308        assert_eq!(SshKeyType::Rsa.to_string(), "ssh-rsa");
309        assert_eq!(SshKeyType::Ed25519.to_string(), "ssh-ed25519");
310        assert_eq!(SshKeyType::Dsa.to_string(), "ssh-dss");
311        assert_eq!(SshKeyType::Ecdsa256.to_string(), "ecdsa-sha2-nistp256");
312        assert_eq!(SshKeyType::Ecdsa384.to_string(), "ecdsa-sha2-nistp384");
313        assert_eq!(SshKeyType::Ecdsa521.to_string(), "ecdsa-sha2-nistp521");
314        assert_eq!(SshKeyType::Unknown.to_string(), "unknown");
315    }
316
317    #[test]
318    fn extract_ssh_keys_no_sshd() {
319        let vaddr: u64 = 0xFFFF_8000_0010_0000;
320        let paddr: u64 = 0x0080_0000;
321        let mut data = vec![0u8; 4096];
322
323        // init_task (PID 1, comm "systemd") — not sshd
324        data[0..4].copy_from_slice(&1u32.to_le_bytes());
325        let tasks_addr = vaddr + 16;
326        data[16..24].copy_from_slice(&tasks_addr.to_le_bytes()); // tasks.next → self
327        data[24..32].copy_from_slice(&tasks_addr.to_le_bytes()); // tasks.prev → self
328        data[32..39].copy_from_slice(b"systemd");
329        data[48..56].copy_from_slice(&0u64.to_le_bytes()); // mm = NULL
330
331        let reader = make_test_reader(&data, vaddr, paddr, &[]);
332        let results = extract_ssh_keys(&reader).unwrap();
333
334        assert!(results.is_empty());
335    }
336
337    #[test]
338    fn extracts_ed25519_key_from_sshd_heap() {
339        let vaddr: u64 = 0xFFFF_8000_0010_0000;
340        let paddr: u64 = 0x0080_0000;
341        let mut data = vec![0u8; 4096];
342
343        // init_task (PID 22, comm "sshd")
344        data[0..4].copy_from_slice(&22u32.to_le_bytes());
345        let tasks_addr = vaddr + 16;
346        data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
347        data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
348        data[32..36].copy_from_slice(b"sshd");
349        let mm_addr = vaddr + 0x200;
350        data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
351
352        // mm_struct at +0x200
353        data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes()); // pgd
354        let vma_addr = vaddr + 0x300;
355        data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes()); // mmap
356
357        // VMA: readable anonymous region
358        let heap_vaddr: u64 = 0x0000_5555_0000_0000;
359        let heap_paddr: u64 = 0x0090_0000;
360        data[0x300..0x308].copy_from_slice(&heap_vaddr.to_le_bytes()); // vm_start
361        data[0x308..0x310].copy_from_slice(&(heap_vaddr + 0x1000).to_le_bytes()); // vm_end
362        data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); // vm_next = NULL
363        data[0x318..0x320].copy_from_slice(&0x3u64.to_le_bytes()); // vm_flags: rw-
364        data[0x320..0x328].copy_from_slice(&0u64.to_le_bytes()); // vm_pgoff
365        data[0x328..0x330].copy_from_slice(&0u64.to_le_bytes()); // vm_file = NULL
366
367        // Heap page with an ed25519 key
368        let mut heap = vec![0u8; 4096];
369        let key_line = b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ user@host\0";
370        heap[0x100..0x100 + key_line.len()].copy_from_slice(key_line);
371
372        let reader = make_test_reader(&data, vaddr, paddr, &[(heap_vaddr, heap_paddr, &heap)]);
373        let results = extract_ssh_keys(&reader).unwrap();
374
375        assert_eq!(results.len(), 1);
376        assert_eq!(results[0].pid, 22);
377        assert_eq!(results[0].key_type, SshKeyType::Ed25519);
378        assert_eq!(
379            results[0].key_data,
380            "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ"
381        );
382        assert_eq!(results[0].comment, "user@host");
383    }
384
385    #[test]
386    fn extracts_rsa_key_without_comment() {
387        let vaddr: u64 = 0xFFFF_8000_0010_0000;
388        let paddr: u64 = 0x0080_0000;
389        let mut data = vec![0u8; 4096];
390
391        // init_task (PID 99, comm "sshd")
392        data[0..4].copy_from_slice(&99u32.to_le_bytes());
393        let tasks_addr = vaddr + 16;
394        data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
395        data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
396        data[32..36].copy_from_slice(b"sshd");
397        let mm_addr = vaddr + 0x200;
398        data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
399
400        // mm_struct at +0x200
401        data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes());
402        let vma_addr = vaddr + 0x300;
403        data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
404
405        // VMA: readable region
406        let heap_vaddr: u64 = 0x0000_5555_0000_0000;
407        let heap_paddr: u64 = 0x0090_0000;
408        data[0x300..0x308].copy_from_slice(&heap_vaddr.to_le_bytes());
409        data[0x308..0x310].copy_from_slice(&(heap_vaddr + 0x1000).to_le_bytes());
410        data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes());
411        data[0x318..0x320].copy_from_slice(&0x1u64.to_le_bytes()); // vm_flags: r--
412        data[0x320..0x328].copy_from_slice(&0u64.to_le_bytes());
413        data[0x328..0x330].copy_from_slice(&0u64.to_le_bytes());
414
415        // Heap page with an RSA key (no comment)
416        let mut heap = vec![0u8; 4096];
417        let key_line = b"ssh-rsa AAAAB3NzaC1yc2EAAA\n";
418        heap[0x200..0x200 + key_line.len()].copy_from_slice(key_line);
419
420        let reader = make_test_reader(&data, vaddr, paddr, &[(heap_vaddr, heap_paddr, &heap)]);
421        let results = extract_ssh_keys(&reader).unwrap();
422
423        assert_eq!(results.len(), 1);
424        assert_eq!(results[0].pid, 99);
425        assert_eq!(results[0].key_type, SshKeyType::Rsa);
426        assert_eq!(results[0].key_data, "ssh-rsa AAAAB3NzaC1yc2EAAA");
427        assert!(results[0].comment.is_empty());
428    }
429
430    #[test]
431    fn deduplicates_identical_keys() {
432        let vaddr: u64 = 0xFFFF_8000_0010_0000;
433        let paddr: u64 = 0x0080_0000;
434        let mut data = vec![0u8; 4096];
435
436        // init_task (PID 10, comm "sshd")
437        data[0..4].copy_from_slice(&10u32.to_le_bytes());
438        let tasks_addr = vaddr + 16;
439        data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
440        data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
441        data[32..36].copy_from_slice(b"sshd");
442        let mm_addr = vaddr + 0x200;
443        data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
444
445        // mm_struct at +0x200
446        data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes());
447        let vma_addr = vaddr + 0x300;
448        data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
449
450        // VMA: single readable region
451        let heap_vaddr: u64 = 0x0000_5555_0000_0000;
452        let heap_paddr: u64 = 0x0090_0000;
453        data[0x300..0x308].copy_from_slice(&heap_vaddr.to_le_bytes());
454        data[0x308..0x310].copy_from_slice(&(heap_vaddr + 0x1000).to_le_bytes());
455        data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes());
456        data[0x318..0x320].copy_from_slice(&0x1u64.to_le_bytes()); // r--
457        data[0x320..0x328].copy_from_slice(&0u64.to_le_bytes());
458        data[0x328..0x330].copy_from_slice(&0u64.to_le_bytes());
459
460        // Same key appears twice in the heap
461        let mut heap = vec![0u8; 4096];
462        let key_line = b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA root@server\0";
463        heap[0x100..0x100 + key_line.len()].copy_from_slice(key_line);
464        heap[0x300..0x300 + key_line.len()].copy_from_slice(key_line);
465
466        let reader = make_test_reader(&data, vaddr, paddr, &[(heap_vaddr, heap_paddr, &heap)]);
467        let results = extract_ssh_keys(&reader).unwrap();
468
469        assert_eq!(results.len(), 1, "duplicate keys should be deduplicated");
470    }
471
472    #[test]
473    fn parse_key_line_ed25519_with_comment() {
474        let (kt, kd, comment) =
475            parse_key_line("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ user@host").unwrap();
476        assert_eq!(kt, SshKeyType::Ed25519);
477        assert_eq!(kd, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ");
478        assert_eq!(comment, "user@host");
479    }
480
481    #[test]
482    fn parse_key_line_rsa_no_comment() {
483        let (kt, kd, comment) = parse_key_line("ssh-rsa AAAAB3NzaC1yc2EAAA").unwrap();
484        assert_eq!(kt, SshKeyType::Rsa);
485        assert_eq!(kd, "ssh-rsa AAAAB3NzaC1yc2EAAA");
486        assert!(comment.is_empty());
487    }
488
489    #[test]
490    fn parse_key_line_invalid() {
491        assert!(parse_key_line("").is_none());
492        assert!(parse_key_line("not-a-key AAAA").is_none());
493        assert!(parse_key_line("ssh-rsa").is_none()); // no base64 data
494    }
495
496    #[test]
497    fn missing_init_task_symbol() {
498        let isf = IsfBuilder::new()
499            .add_struct("task_struct", 64)
500            .add_field("task_struct", "pid", 0, "int")
501            .add_field("task_struct", "tasks", 8, "list_head")
502            .add_struct("list_head", 16)
503            .add_field("list_head", "next", 0, "pointer")
504            .add_field("list_head", "prev", 8, "pointer")
505            .build_json();
506
507        let resolver = IsfResolver::from_value(&isf).unwrap();
508        let (cr3, mem) = PageTableBuilder::new().build();
509        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
510        let reader = ObjectReader::new(vas, Box::new(resolver));
511
512        let result = extract_ssh_keys(&reader);
513        assert!(
514            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "init_task"),
515            "expected MissingKernelSymbol {{name: \"init_task\"}}, got {result:?}"
516        );
517    }
518
519    #[test]
520    fn missing_tasks_field_returns_missing_field() {
521        let isf = IsfBuilder::new()
522            .add_struct("task_struct", 64)
523            .add_field("task_struct", "pid", 0, "int")
524            // tasks intentionally omitted
525            .add_struct("list_head", 16)
526            .add_field("list_head", "next", 0, "pointer")
527            .add_field("list_head", "prev", 8, "pointer")
528            .add_symbol("init_task", 0xFFFF_8000_0010_0000)
529            .build_json();
530        let resolver = IsfResolver::from_value(&isf).unwrap();
531        let (cr3, mem) = PageTableBuilder::new().build();
532        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
533        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
534        let result = extract_ssh_keys(&reader);
535        assert!(
536            matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "task_struct" && field_name == "tasks"),
537            "expected MissingField task_struct.tasks, got {result:?}"
538        );
539    }
540}