use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use vmm_sys_util::eventfd::EventFd;
use super::super::vcpu::{ImmediateExitHandle, WatchpointArm, vcpu_signal};
use super::state::SnapshotRequest;
pub(super) fn frame_snapshot_reply(request_id: u32, status: u32, reason: &str) -> Vec<u8> {
use crate::vmm::wire::{
FRAME_HEADER_SIZE, MSG_TYPE_SNAPSHOT_REPLY, SNAPSHOT_REASON_MAX, ShmMessage,
SnapshotReplyPayload,
};
use zerocopy::IntoBytes;
let reason_bytes = reason.as_bytes();
let reason_len = reason_bytes.len().min(SNAPSHOT_REASON_MAX);
let mut reason_buf = [0u8; SNAPSHOT_REASON_MAX];
reason_buf[..reason_len].copy_from_slice(&reason_bytes[..reason_len]);
let payload = SnapshotReplyPayload {
request_id,
status,
reason: reason_buf,
};
let payload_bytes = payload.as_bytes();
let header = ShmMessage {
msg_type: MSG_TYPE_SNAPSHOT_REPLY,
length: payload_bytes.len() as u32,
crc32: crc32fast::hash(payload_bytes),
_pad: 0,
};
let mut buf = Vec::with_capacity(FRAME_HEADER_SIZE + payload_bytes.len());
buf.extend_from_slice(header.as_bytes());
buf.extend_from_slice(payload_bytes);
buf
}
pub(super) fn decode_snapshot_request(payload: &[u8]) -> Option<SnapshotRequest> {
use crate::vmm::wire::{SNAPSHOT_KIND_NONE, SNAPSHOT_TAG_MAX, SnapshotRequestPayload};
use zerocopy::FromBytes;
if payload.len() != std::mem::size_of::<SnapshotRequestPayload>() {
return None;
}
let req = SnapshotRequestPayload::read_from_bytes(payload).ok()?;
if req.request_id == 0 || req.kind == SNAPSHOT_KIND_NONE {
return None;
}
let len = req
.tag
.iter()
.position(|&b| b == 0)
.unwrap_or(SNAPSHOT_TAG_MAX);
let tag = String::from_utf8_lossy(&req.tag[..len]).to_string();
Some(SnapshotRequest {
request_id: req.request_id,
kind: req.kind,
tag,
})
}
pub(super) struct VmlinuxSymbolCache {
symbols: std::collections::HashMap<String, u64>,
}
impl VmlinuxSymbolCache {
pub(super) fn from_path(path: &std::path::Path) -> std::result::Result<Self, String> {
const SHN_UNDEF: usize = 0;
let data =
std::fs::read(path).map_err(|e| format!("read vmlinux at {}: {e}", path.display()))?;
let elf = goblin::elf::Elf::parse(&data).map_err(|e| format!("parse vmlinux ELF: {e}"))?;
let mut symbols = std::collections::HashMap::new();
for s in elf.syms.iter() {
if s.st_shndx == SHN_UNDEF {
continue;
}
if let Some(name) = elf.strtab.get_at(s.st_name) {
symbols.insert(name.to_string(), s.st_value);
}
}
Ok(Self { symbols })
}
pub(super) fn lookup(&self, symbol: &str) -> Option<u64> {
self.symbols.get(symbol).copied()
}
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn from_symbols_for_test(symbols: std::collections::HashMap<String, u64>) -> Self {
Self { symbols }
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn arm_user_watchpoint(
watchpoint: &Arc<WatchpointArm>,
symbol_cache: &VmlinuxSymbolCache,
symbol: &str,
ap_pthreads: &[libc::pthread_t],
ap_ies: &[Option<ImmediateExitHandle>],
ap_alive: &[Arc<AtomicBool>],
bsp_tid: libc::pthread_t,
bsp_ie: Option<&ImmediateExitHandle>,
bsp_alive: &Arc<AtomicBool>,
) -> std::result::Result<usize, String> {
let mut free_slot: Option<usize> = None;
for (i, slot) in watchpoint.user.iter().enumerate() {
if slot.request_kva.load(Ordering::Acquire) == 0 {
free_slot = Some(i);
break;
}
}
let Some(idx) = free_slot else {
return Err(format!(
"no free user watchpoint slot — slots 1..=3 all occupied by prior \
Op::WatchSnapshot registrations (cap = {})",
watchpoint.user.len()
));
};
let kva = symbol_cache
.lookup(symbol)
.ok_or_else(|| format!("symbol '{symbol}' not found in vmlinux symtab"))?;
if kva == 0 {
return Err(format!(
"symbol '{symbol}' resolved to KVA 0 — defined symbol at \
address 0 is not arm-able (slot's `request_kva == 0` is \
the free-slot sentinel; arming would be a silent no-op)"
));
}
if kva & 0x3 != 0 {
return Err(format!(
"symbol '{symbol}' KVA {kva:#x} is not 4-byte aligned. \
x86_64 DR_LEN_4 watchpoints (Intel SDM Vol. 3B Ch. 17) \
and aarch64 DBGWVR (ARM ARM D7.3.10, requires VA[1:0] = \
00) both require 4-byte aligned targets for the 4-byte \
write-watch the failure-dump trigger uses"
));
}
{
let mut tag_guard = watchpoint.user[idx]
.tag
.lock()
.unwrap_or_else(|e| e.into_inner());
*tag_guard = symbol.to_string();
}
watchpoint.user[idx]
.request_kva
.store(kva, Ordering::Release);
watchpoint.mark_armed();
for (i, ie) in ap_ies.iter().enumerate() {
if let Some(ie) = ie
&& ap_alive[i].load(Ordering::Acquire)
{
ie.set(1);
}
}
if bsp_alive.load(Ordering::Acquire)
&& let Some(ie) = bsp_ie
{
ie.set(1);
}
std::sync::atomic::fence(Ordering::Release);
for &tid in ap_pthreads {
unsafe {
libc::pthread_kill(tid, vcpu_signal());
}
}
if bsp_alive.load(Ordering::Acquire) {
unsafe {
libc::pthread_kill(bsp_tid, vcpu_signal());
}
}
Ok(idx)
}
pub(super) fn snapshot_tagged_path(base: &std::path::Path, tag: &str) -> std::path::PathBuf {
let mut tagged = base.to_path_buf();
let raw_stem = base
.file_stem()
.and_then(|s| s.to_str())
.filter(|s| !s.is_empty() && !s.starts_with('.'))
.unwrap_or("dump");
let stem = raw_stem.strip_suffix(".failure-dump").unwrap_or(raw_stem);
let ext = base.extension().and_then(|e| e.to_str());
let safe_tag: String = tag
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect();
let new_name = match ext {
Some(ext) => format!("{stem}.snapshot.{safe_tag}.{ext}"),
None => format!("{stem}.snapshot.{safe_tag}"),
};
tagged.set_file_name(new_name);
tagged
}
pub(super) fn poll_eventfd_until_ready_or_timeout(evt: &EventFd, timeout_ms: i32) {
use std::os::fd::AsRawFd;
let mut pfd = libc::pollfd {
fd: evt.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
unsafe {
libc::poll(&mut pfd, 1, timeout_ms);
}
}
#[cfg(test)]
mod snapshot_tagged_path_tests {
use super::snapshot_tagged_path;
use std::path::{Path, PathBuf};
#[test]
fn strips_failure_dump_suffix_from_stem() {
let base = Path::new("/tmp/run/coord.failure-dump.json");
let out = snapshot_tagged_path(base, "mid_run");
assert_eq!(out, PathBuf::from("/tmp/run/coord.snapshot.mid_run.json"));
}
#[test]
fn sanitises_path_traversal_in_tag() {
let base = Path::new("/tmp/run/coord.failure-dump.json");
let out = snapshot_tagged_path(base, "../etc/passwd");
assert_eq!(
out,
PathBuf::from("/tmp/run/coord.snapshot..._etc_passwd.json")
);
}
#[test]
fn sanitises_nul_and_shell_metachars_in_tag() {
let base = Path::new("/tmp/run/coord.failure-dump.json");
let out = snapshot_tagged_path(base, "x\0y;rm -rf");
assert_eq!(
out,
PathBuf::from("/tmp/run/coord.snapshot.x_y_rm_-rf.json")
);
}
#[test]
fn preserves_allowed_chars_in_tag() {
let base = Path::new("/tmp/run/coord.failure-dump.json");
let out = snapshot_tagged_path(base, "Tag.1_v-2");
assert_eq!(out, PathBuf::from("/tmp/run/coord.snapshot.Tag.1_v-2.json"));
}
#[test]
fn no_extension_path_omits_extension() {
let base = Path::new("/tmp/run/coord");
let out = snapshot_tagged_path(base, "tag1");
assert_eq!(out, PathBuf::from("/tmp/run/coord.snapshot.tag1"));
}
#[test]
fn no_stem_path_falls_back_to_dump() {
let base = Path::new("/tmp/run/.json");
let out = snapshot_tagged_path(base, "tag1");
assert_eq!(out, PathBuf::from("/tmp/run/dump.snapshot.tag1"));
}
#[test]
fn restripping_after_first_tag_is_idempotent_per_call() {
let base = Path::new("/tmp/run/coord.failure-dump.json");
let once = snapshot_tagged_path(base, "t1");
assert_eq!(once, PathBuf::from("/tmp/run/coord.snapshot.t1.json"));
let twice = snapshot_tagged_path(&once, "t2");
assert_eq!(
twice,
PathBuf::from("/tmp/run/coord.snapshot.t1.snapshot.t2.json")
);
let again = snapshot_tagged_path(base, "t1");
assert_eq!(again, once);
}
#[test]
fn stem_without_failure_dump_suffix_is_preserved() {
let base = Path::new("/tmp/run/coord.json");
let out = snapshot_tagged_path(base, "tag1");
assert_eq!(out, PathBuf::from("/tmp/run/coord.snapshot.tag1.json"));
}
}
#[cfg(test)]
mod vmlinux_symbol_cache_tests {
use super::VmlinuxSymbolCache;
use std::collections::HashMap;
#[test]
fn lookup_returns_inserted_kva() {
let mut symbols = HashMap::new();
symbols.insert("scx_root".to_string(), 0xffff_8000_0000_4000u64);
symbols.insert(
"ktstr_err_exit_detected".to_string(),
0xffff_8000_0000_8000u64,
);
let cache = VmlinuxSymbolCache::from_symbols_for_test(symbols);
assert_eq!(cache.lookup("scx_root"), Some(0xffff_8000_0000_4000u64));
assert_eq!(
cache.lookup("ktstr_err_exit_detected"),
Some(0xffff_8000_0000_8000u64)
);
}
#[test]
fn lookup_returns_none_for_missing_symbol() {
let mut symbols = HashMap::new();
symbols.insert("scx_root".to_string(), 0xffff_8000_0000_4000u64);
let cache = VmlinuxSymbolCache::from_symbols_for_test(symbols);
assert_eq!(cache.lookup("nonexistent_symbol"), None);
assert_eq!(cache.lookup(""), None);
}
#[test]
fn duplicate_symbol_resolves_to_last_inserted() {
let mut symbols = HashMap::new();
symbols.insert("dup_sym".to_string(), 0x1000u64);
let prior = symbols.insert("dup_sym".to_string(), 0x2000u64);
assert_eq!(prior, Some(0x1000u64));
let cache = VmlinuxSymbolCache::from_symbols_for_test(symbols);
assert_eq!(cache.lookup("dup_sym"), Some(0x2000u64));
}
#[test]
fn empty_cache_lookup_always_none() {
let cache = VmlinuxSymbolCache::from_symbols_for_test(HashMap::new());
assert_eq!(cache.lookup("any"), None);
assert_eq!(cache.lookup("scx_root"), None);
}
}
#[cfg(test)]
mod arm_user_watchpoint_tests {
use super::{VmlinuxSymbolCache, arm_user_watchpoint};
use crate::vmm::vcpu::WatchpointArm;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
fn dead_bsp() -> Arc<AtomicBool> {
Arc::new(AtomicBool::new(false))
}
fn cache_with(name: &str, kva: u64) -> VmlinuxSymbolCache {
let mut m = HashMap::new();
m.insert(name.to_string(), kva);
VmlinuxSymbolCache::from_symbols_for_test(m)
}
#[test]
fn rejects_unaligned_kva() {
let wp = Arc::new(WatchpointArm::new().unwrap());
let cache = cache_with("misaligned_sym", 0xffff_8000_0000_4001u64);
let bsp_alive = dead_bsp();
let err = arm_user_watchpoint(
&wp,
&cache,
"misaligned_sym",
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.unwrap_err();
assert!(
err.contains("4-byte aligned") || err.contains("4-byte"),
"unaligned-KVA error must mention 4-byte alignment, got: {err}"
);
for slot in &wp.user {
assert_eq!(
slot.request_kva.load(Ordering::Acquire),
0,
"rejected arm must not write request_kva"
);
}
}
#[test]
fn rejects_missing_symbol() {
let wp = Arc::new(WatchpointArm::new().unwrap());
let cache = cache_with("present_sym", 0xffff_8000_0000_4000u64);
let bsp_alive = dead_bsp();
let err = arm_user_watchpoint(
&wp,
&cache,
"absent_sym",
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.unwrap_err();
assert!(
err.contains("absent_sym"),
"missing-symbol error must name the symbol, got: {err}"
);
for slot in &wp.user {
assert_eq!(
slot.request_kva.load(Ordering::Acquire),
0,
"rejected arm must not write request_kva"
);
}
}
#[test]
fn rejects_zero_kva_explicitly() {
let wp = Arc::new(WatchpointArm::new().unwrap());
let cache = cache_with("zero_sym", 0);
let bsp_alive = dead_bsp();
let err = arm_user_watchpoint(
&wp,
&cache,
"zero_sym",
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.unwrap_err();
assert!(
err.contains("KVA 0") || err.contains("zero_sym"),
"zero-KVA error must mention the symbol or zero, got: {err}"
);
for slot in &wp.user {
assert_eq!(
slot.request_kva.load(Ordering::Acquire),
0,
"rejected zero-KVA arm must not write request_kva"
);
}
}
#[test]
fn successful_arm_consumes_first_free_slot() {
let wp = Arc::new(WatchpointArm::new().unwrap());
let kva = 0xffff_8000_0000_4000u64;
let cache = cache_with("scx_root", kva);
let bsp_alive = dead_bsp();
let idx = arm_user_watchpoint(
&wp,
&cache,
"scx_root",
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.expect("aligned valid symbol must arm");
assert_eq!(idx, 0, "first free slot is index 0");
assert_eq!(
wp.user[0].request_kva.load(Ordering::Acquire),
kva,
"slot 0 must hold the resolved KVA"
);
let tag = wp.user[0].tag.lock().unwrap().clone();
assert_eq!(tag, "scx_root", "slot 0 tag must match symbol name");
assert_eq!(
wp.any_armed.load(Ordering::Relaxed),
1,
"any_armed gate must flip to 1 after successful arm"
);
for sibling in 1..3 {
assert_eq!(
wp.user[sibling].request_kva.load(Ordering::Acquire),
0,
"sibling slot {sibling} must remain unarmed"
);
}
}
#[test]
fn arms_consume_slots_in_index_order() {
let wp = Arc::new(WatchpointArm::new().unwrap());
let bsp_alive = dead_bsp();
let mut symbols = HashMap::new();
symbols.insert("sym_a".to_string(), 0xffff_8000_0000_4000u64);
symbols.insert("sym_b".to_string(), 0xffff_8000_0000_5000u64);
let cache = VmlinuxSymbolCache::from_symbols_for_test(symbols);
let i0 = arm_user_watchpoint(
&wp,
&cache,
"sym_a",
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.unwrap();
let i1 = arm_user_watchpoint(
&wp,
&cache,
"sym_b",
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.unwrap();
assert_eq!(i0, 0);
assert_eq!(i1, 1);
assert_eq!(
wp.user[0].request_kva.load(Ordering::Acquire),
0xffff_8000_0000_4000u64
);
assert_eq!(
wp.user[1].request_kva.load(Ordering::Acquire),
0xffff_8000_0000_5000u64
);
assert_eq!(
wp.user[2].request_kva.load(Ordering::Acquire),
0,
"third slot must remain unarmed when only two arms ran"
);
}
#[test]
fn arm_returns_error_when_all_slots_occupied() {
let wp = Arc::new(WatchpointArm::new().unwrap());
let bsp_alive = dead_bsp();
let mut symbols = HashMap::new();
symbols.insert("sym_a".to_string(), 0xffff_8000_0000_4000u64);
symbols.insert("sym_b".to_string(), 0xffff_8000_0000_5000u64);
symbols.insert("sym_c".to_string(), 0xffff_8000_0000_6000u64);
symbols.insert("sym_d".to_string(), 0xffff_8000_0000_7000u64);
let cache = VmlinuxSymbolCache::from_symbols_for_test(symbols);
for sym in &["sym_a", "sym_b", "sym_c"] {
arm_user_watchpoint(
&wp,
&cache,
sym,
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.expect("first three arms succeed");
}
let err = arm_user_watchpoint(
&wp,
&cache,
"sym_d",
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.unwrap_err();
assert!(
err.contains("no free user watchpoint slot")
|| err.contains("slots 1..=3 all occupied"),
"exhaustion error must mention slot capacity, got: {err}"
);
assert_eq!(
wp.user[0].request_kva.load(Ordering::Acquire),
0xffff_8000_0000_4000u64
);
assert_eq!(
wp.user[1].request_kva.load(Ordering::Acquire),
0xffff_8000_0000_5000u64
);
assert_eq!(
wp.user[2].request_kva.load(Ordering::Acquire),
0xffff_8000_0000_6000u64
);
}
#[test]
fn slot_becomes_reusable_after_request_kva_cleared() {
let wp = Arc::new(WatchpointArm::new().unwrap());
let bsp_alive = dead_bsp();
let mut symbols = HashMap::new();
symbols.insert("sym_a".to_string(), 0xffff_8000_0000_4000u64);
symbols.insert("sym_b".to_string(), 0xffff_8000_0000_5000u64);
symbols.insert("sym_c".to_string(), 0xffff_8000_0000_6000u64);
symbols.insert("sym_d".to_string(), 0xffff_8000_0000_8000u64);
let cache = VmlinuxSymbolCache::from_symbols_for_test(symbols);
for sym in &["sym_a", "sym_b", "sym_c"] {
arm_user_watchpoint(
&wp,
&cache,
sym,
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.unwrap();
}
wp.user[1].request_kva.store(0, Ordering::Release);
let idx = arm_user_watchpoint(
&wp,
&cache,
"sym_d",
&[],
&[],
&[],
0 as libc::pthread_t,
None,
&bsp_alive,
)
.unwrap();
assert_eq!(idx, 1, "freed slot 1 must be reused before slot 2");
assert_eq!(
wp.user[1].request_kva.load(Ordering::Acquire),
0xffff_8000_0000_8000u64,
"freed slot now holds the new KVA"
);
assert_eq!(
wp.user[0].request_kva.load(Ordering::Acquire),
0xffff_8000_0000_4000u64
);
assert_eq!(
wp.user[2].request_kva.load(Ordering::Acquire),
0xffff_8000_0000_6000u64
);
}
}