use std::io::{self, Read, Write};
use crate::snapshot_frame::cap_count;
pub type Vpid = u32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SocketHandle(pub u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum EgressProto {
Tcp = 0,
Udp = 1,
}
impl EgressProto {
fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(Self::Tcp),
1 => Some(Self::Udp),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum DevKind {
Null = 0,
Zero = 1,
Full = 2,
Random = 3,
Tty = 4,
}
impl DevKind {
fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(Self::Null),
1 => Some(Self::Zero),
2 => Some(Self::Full),
3 => Some(Self::Random),
4 => Some(Self::Tty),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum PipeEnd {
Read = 0,
Write = 1,
}
impl PipeEnd {
fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(Self::Read),
1 => Some(Self::Write),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EpollLoopWatch {
pub socket: SocketHandle,
pub events: u32,
pub data: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FdTarget {
LoopSocket(SocketHandle),
RootRelFile {
path: Vec<u8>,
offset: u64,
flags: u32,
},
HostEgressSocket {
proto: EgressProto,
v6: bool,
local_ip: [u8; 16],
local_port: u16,
connected: bool,
remote_ip: [u8; 16],
remote_port: u16,
},
DevNode {
kind: DevKind,
flags: u32,
},
Pipe {
id: u64,
end: PipeEnd,
buffered: Vec<u8>,
flags: u32,
},
Epoll {
loop_watches: Vec<EpollLoopWatch>,
},
}
const BLOB_MAX: usize = 64 << 20;
#[inline]
fn rb<const N: usize, R: Read>(r: &mut R) -> io::Result<[u8; N]> {
let mut x = [0u8; N];
r.read_exact(&mut x)?;
Ok(x)
}
#[inline]
fn ru8<R: Read>(r: &mut R) -> io::Result<u8> {
Ok(rb::<1, _>(r)?[0])
}
#[inline]
fn ru16<R: Read>(r: &mut R) -> io::Result<u16> {
Ok(u16::from_le_bytes(rb::<2, _>(r)?))
}
#[inline]
fn ru32<R: Read>(r: &mut R) -> io::Result<u32> {
Ok(u32::from_le_bytes(rb::<4, _>(r)?))
}
#[inline]
fn ru64<R: Read>(r: &mut R) -> io::Result<u64> {
Ok(u64::from_le_bytes(rb::<8, _>(r)?))
}
#[inline]
fn ri64<R: Read>(r: &mut R) -> io::Result<i64> {
Ok(i64::from_le_bytes(rb::<8, _>(r)?))
}
#[inline]
fn err(m: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, m.into())
}
#[inline]
fn wblob<W: Write>(w: &mut W, b: &[u8]) -> io::Result<()> {
w.write_all(&(b.len() as u32).to_le_bytes())?;
w.write_all(b)
}
#[inline]
fn rblob<R: Read>(r: &mut R, what: &str) -> io::Result<Vec<u8>> {
let n = ru32(r)? as usize;
if n > BLOB_MAX {
return Err(err(format!("{what} blob length {n} exceeds {BLOB_MAX}")));
}
let mut buf = vec![0u8; n];
r.read_exact(&mut buf)?;
Ok(buf)
}
#[inline]
fn read_count<R: Read>(r: &mut R, min_elem: usize) -> io::Result<(usize, usize)> {
let count = ru32(r)? as usize;
let hint = cap_count(count, min_elem, BLOB_MAX);
Ok((count, hint))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProcState {
Running,
Zombie(i32),
Reaped,
}
impl ProcState {
fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
match self {
ProcState::Running => w.write_all(&[0u8]),
ProcState::Zombie(code) => {
w.write_all(&[1u8])?;
w.write_all(&code.to_le_bytes())
}
ProcState::Reaped => w.write_all(&[2u8]),
}
}
fn read_from<R: Read>(r: &mut R) -> io::Result<ProcState> {
Ok(match ru8(r)? {
0 => ProcState::Running,
1 => ProcState::Zombie(i32::from_le_bytes(rb::<4, _>(r)?)),
2 => ProcState::Reaped,
t => return Err(err(format!("bad ProcState tag {t}"))),
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ThreadRegs {
pub vtid: u32,
pub gregs: [i64; 18],
pub fs: u64,
pub xsave: Vec<u8>,
}
impl ThreadRegs {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.vtid.to_le_bytes())?;
for g in &self.gregs {
w.write_all(&g.to_le_bytes())?;
}
w.write_all(&self.fs.to_le_bytes())?;
wblob(w, &self.xsave)
}
pub fn read_from<R: Read>(r: &mut R) -> io::Result<ThreadRegs> {
let vtid = ru32(r)?;
let mut gregs = [0i64; 18];
for g in &mut gregs {
*g = ri64(r)?;
}
let fs = ru64(r)?;
let xsave = rblob(r, "xsave")?;
Ok(ThreadRegs {
vtid,
gregs,
fs,
xsave,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GuestProcRecord {
pub vpid: Vpid,
pub parent_vpid: Vpid,
pub pgid: u32,
pub sid: u32,
pub threads: Vec<ThreadRegs>,
pub state: ProcState,
}
impl GuestProcRecord {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.vpid.to_le_bytes())?;
w.write_all(&self.parent_vpid.to_le_bytes())?;
w.write_all(&self.pgid.to_le_bytes())?;
w.write_all(&self.sid.to_le_bytes())?;
w.write_all(&(self.threads.len() as u32).to_le_bytes())?;
for t in &self.threads {
t.write_to(w)?;
}
self.state.write_to(w)
}
pub fn read_from<R: Read>(r: &mut R) -> io::Result<GuestProcRecord> {
let vpid = ru32(r)?;
let parent_vpid = ru32(r)?;
let pgid = ru32(r)?;
let sid = ru32(r)?;
let (n, hint) = read_count(r, 160)?;
let mut threads = Vec::with_capacity(hint);
for _ in 0..n {
threads.push(ThreadRegs::read_from(r)?);
}
let state = ProcState::read_from(r)?;
Ok(GuestProcRecord {
vpid,
parent_vpid,
pgid,
sid,
threads,
state,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VmaRecord {
pub va_start: u64,
pub len: u64,
pub prot: u32,
pub file_off: u64,
}
impl VmaRecord {
pub const WIRE_LEN: usize = 28;
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.va_start.to_le_bytes())?;
w.write_all(&self.len.to_le_bytes())?;
w.write_all(&self.prot.to_le_bytes())?;
w.write_all(&self.file_off.to_le_bytes())?;
Ok(())
}
pub fn read_from<R: Read>(r: &mut R) -> io::Result<VmaRecord> {
Ok(VmaRecord {
va_start: ru64(r)?,
len: ru64(r)?,
prot: ru32(r)?,
file_off: ru64(r)?,
})
}
}
impl FdTarget {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
match self {
FdTarget::LoopSocket(h) => {
w.write_all(&[0u8])?;
w.write_all(&h.0.to_le_bytes())?;
}
FdTarget::RootRelFile {
path,
offset,
flags,
} => {
w.write_all(&[1u8])?;
wblob(w, path)?;
w.write_all(&offset.to_le_bytes())?;
w.write_all(&flags.to_le_bytes())?;
}
FdTarget::HostEgressSocket {
proto,
v6,
local_ip,
local_port,
connected,
remote_ip,
remote_port,
} => {
w.write_all(&[2u8])?;
w.write_all(&[*proto as u8, *v6 as u8, *connected as u8])?;
w.write_all(local_ip)?;
w.write_all(&local_port.to_le_bytes())?;
w.write_all(remote_ip)?;
w.write_all(&remote_port.to_le_bytes())?;
}
FdTarget::DevNode { kind, flags } => {
w.write_all(&[3u8, *kind as u8])?;
w.write_all(&flags.to_le_bytes())?;
}
FdTarget::Pipe {
id,
end,
buffered,
flags,
} => {
w.write_all(&[4u8, *end as u8])?;
w.write_all(&id.to_le_bytes())?;
wblob(w, buffered)?;
w.write_all(&flags.to_le_bytes())?;
}
FdTarget::Epoll { loop_watches } => {
w.write_all(&[5u8])?;
w.write_all(&(loop_watches.len() as u32).to_le_bytes())?;
for watch in loop_watches {
w.write_all(&watch.socket.0.to_le_bytes())?;
w.write_all(&watch.events.to_le_bytes())?;
w.write_all(&watch.data.to_le_bytes())?;
}
}
}
Ok(())
}
pub fn read_from<R: Read>(r: &mut R) -> io::Result<FdTarget> {
Ok(match ru8(r)? {
0 => FdTarget::LoopSocket(SocketHandle(ru32(r)?)),
1 => FdTarget::RootRelFile {
path: rblob(r, "fd.path")?,
offset: ru64(r)?,
flags: ru32(r)?,
},
2 => {
let proto = EgressProto::from_u8(ru8(r)?).ok_or_else(|| err("bad EgressProto"))?;
let v6 = ru8(r)? != 0;
let connected = ru8(r)? != 0;
let local_ip = rb::<16, _>(r)?;
let local_port = ru16(r)?;
let remote_ip = rb::<16, _>(r)?;
let remote_port = ru16(r)?;
FdTarget::HostEgressSocket {
proto,
v6,
local_ip,
local_port,
connected,
remote_ip,
remote_port,
}
}
3 => FdTarget::DevNode {
kind: DevKind::from_u8(ru8(r)?).ok_or_else(|| err("bad DevKind"))?,
flags: ru32(r)?,
},
4 => {
let end = PipeEnd::from_u8(ru8(r)?).ok_or_else(|| err("bad PipeEnd"))?;
FdTarget::Pipe {
id: ru64(r)?,
end,
buffered: rblob(r, "fd.pipe")?,
flags: ru32(r)?,
}
}
5 => {
let (count, hint) = read_count(r, 16)?;
let mut loop_watches = Vec::with_capacity(hint);
for _ in 0..count {
loop_watches.push(EpollLoopWatch {
socket: SocketHandle(ru32(r)?),
events: ru32(r)?,
data: ru64(r)?,
});
}
FdTarget::Epoll { loop_watches }
}
t => return Err(err(format!("bad FdTarget tag {t}"))),
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ProcMemImage {
pub vpid: Vpid,
pub blob_off: u64,
pub regions: Vec<VmaRecord>,
pub threads: Vec<ThreadRegs>,
}
impl ProcMemImage {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.vpid.to_le_bytes())?;
w.write_all(&self.blob_off.to_le_bytes())?;
w.write_all(&(self.regions.len() as u32).to_le_bytes())?;
for v in &self.regions {
v.write_to(w)?;
}
w.write_all(&(self.threads.len() as u32).to_le_bytes())?;
for t in &self.threads {
t.write_to(w)?;
}
Ok(())
}
pub fn read_from<R: Read>(r: &mut R) -> io::Result<ProcMemImage> {
let vpid = ru32(r)?;
let blob_off = ru64(r)?;
let (n, hint) = read_count(r, VmaRecord::WIRE_LEN)?;
let mut regions = Vec::with_capacity(hint);
for _ in 0..n {
regions.push(VmaRecord::read_from(r)?);
}
let (nt, hint) = read_count(r, 160)?;
let mut threads = Vec::with_capacity(hint);
for _ in 0..nt {
threads.push(ThreadRegs::read_from(r)?);
}
Ok(ProcMemImage {
vpid,
blob_off,
regions,
threads,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct MemImage {
pub regions: Vec<VmaRecord>,
pub threads: Vec<ThreadRegs>,
pub procs: Vec<ProcMemImage>,
}
impl MemImage {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&(self.regions.len() as u32).to_le_bytes())?;
for v in &self.regions {
v.write_to(w)?;
}
w.write_all(&(self.threads.len() as u32).to_le_bytes())?;
for t in &self.threads {
t.write_to(w)?;
}
w.write_all(&(self.procs.len() as u32).to_le_bytes())?;
for p in &self.procs {
p.write_to(w)?;
}
Ok(())
}
pub fn read_from<R: Read>(r: &mut R) -> io::Result<MemImage> {
let (n, hint) = read_count(r, VmaRecord::WIRE_LEN)?;
let mut regions = Vec::with_capacity(hint);
for _ in 0..n {
regions.push(VmaRecord::read_from(r)?);
}
let (nt, hint) = read_count(r, 160)?;
let mut threads = Vec::with_capacity(hint);
for _ in 0..nt {
threads.push(ThreadRegs::read_from(r)?);
}
let (np, hint) = read_count(r, 20)?;
let mut procs = Vec::with_capacity(hint);
for _ in 0..np {
procs.push(ProcMemImage::read_from(r)?);
}
Ok(MemImage {
regions,
threads,
procs,
})
}
pub fn proc_image(&self, vpid: Vpid) -> Option<ProcMemImageRef<'_>> {
if let Some(p) = self.procs.iter().find(|p| p.vpid == vpid) {
return Some(ProcMemImageRef {
blob_off: p.blob_off,
regions: &p.regions,
threads: &p.threads,
});
}
if vpid == 1 && (!self.regions.is_empty() || !self.threads.is_empty()) {
return Some(ProcMemImageRef {
blob_off: 0,
regions: &self.regions,
threads: &self.threads,
});
}
None
}
}
pub struct ProcMemImageRef<'a> {
pub blob_off: u64,
pub regions: &'a [VmaRecord],
pub threads: &'a [ThreadRegs],
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ProcTreeSnapshot {
pub procs: Vec<GuestProcRecord>,
}
impl ProcTreeSnapshot {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&(self.procs.len() as u32).to_le_bytes())?;
for p in &self.procs {
p.write_to(w)?;
}
Ok(())
}
pub fn read_from<R: Read>(r: &mut R) -> io::Result<ProcTreeSnapshot> {
let (n, hint) = read_count(r, 21)?;
let mut procs = Vec::with_capacity(hint);
for _ in 0..n {
procs.push(GuestProcRecord::read_from(r)?);
}
Ok(ProcTreeSnapshot { procs })
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct FdTableSnapshot {
pub per_vpid: Vec<(Vpid, Vec<(i32, FdTarget)>)>,
}
impl FdTableSnapshot {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&(self.per_vpid.len() as u32).to_le_bytes())?;
for (vpid, fds) in &self.per_vpid {
w.write_all(&vpid.to_le_bytes())?;
w.write_all(&(fds.len() as u32).to_le_bytes())?;
for (gfd, target) in fds {
w.write_all(&gfd.to_le_bytes())?;
target.write_to(w)?;
}
}
Ok(())
}
pub fn read_from<R: Read>(r: &mut R) -> io::Result<FdTableSnapshot> {
let (n, hint) = read_count(r, 8)?;
let mut per_vpid = Vec::with_capacity(hint);
for _ in 0..n {
let vpid = ru32(r)?;
let (nf, fhint) = read_count(r, 5)?;
let mut fds = Vec::with_capacity(fhint);
for _ in 0..nf {
let gfd = i32::from_le_bytes(rb::<4, _>(r)?);
let target = FdTarget::read_from(r)?;
fds.push((gfd, target));
}
per_vpid.push((vpid, fds));
}
Ok(FdTableSnapshot { per_vpid })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CpuFeatures {
bits: u64,
}
impl CpuFeatures {
pub const SSE2: u64 = 1 << 0;
pub const AVX: u64 = 1 << 1;
pub const AVX2: u64 = 1 << 2;
pub const AVX512F: u64 = 1 << 3;
pub const FMA: u64 = 1 << 4;
pub const BMI2: u64 = 1 << 5;
pub fn from_bits(bits: u64) -> CpuFeatures {
CpuFeatures { bits }
}
pub fn bits(&self) -> u64 {
self.bits
}
pub fn current() -> CpuFeatures {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
let mut bits = 0u64;
if std::is_x86_feature_detected!("sse2") {
bits |= Self::SSE2;
}
if std::is_x86_feature_detected!("avx") {
bits |= Self::AVX;
}
if std::is_x86_feature_detected!("avx2") {
bits |= Self::AVX2;
}
if std::is_x86_feature_detected!("avx512f") {
bits |= Self::AVX512F;
}
if std::is_x86_feature_detected!("fma") {
bits |= Self::FMA;
}
if std::is_x86_feature_detected!("bmi2") {
bits |= Self::BMI2;
}
CpuFeatures { bits }
}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
{
CpuFeatures { bits: 0 }
}
}
pub fn satisfied_by(&self, host: &CpuFeatures) -> bool {
(self.bits & !host.bits) == 0
}
fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.bits.to_le_bytes())
}
fn read_from<R: Read>(r: &mut R) -> io::Result<CpuFeatures> {
Ok(CpuFeatures { bits: ru64(r)? })
}
}
const MAGIC: [u8; 8] = *b"SMSTSNP1";
pub const CURRENT_VERSION: u32 = 7;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SnapshotEntryKind {
WarmZygote,
LiveTree,
}
impl SnapshotEntryKind {
fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
let tag = match self {
SnapshotEntryKind::WarmZygote => 0u8,
SnapshotEntryKind::LiveTree => 1u8,
};
w.write_all(&[tag])
}
fn read_from<R: Read>(r: &mut R) -> io::Result<SnapshotEntryKind> {
let mut tag = [0u8; 1];
r.read_exact(&mut tag)?;
match tag[0] {
0 => Ok(SnapshotEntryKind::WarmZygote),
1 => Ok(SnapshotEntryKind::LiveTree),
other => Err(err(format!("invalid snapshot entry-kind tag {other}"))),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StateSnapshot {
pub version: u32,
pub cpu_feature_baseline: CpuFeatures,
pub entry_kind: SnapshotEntryKind,
pub mem: MemImage,
pub proctree: ProcTreeSnapshot,
pub netstack: super::netstack::LoopSnapshot,
pub fdtable: FdTableSnapshot,
pub cwds: Vec<(Vpid, Vec<u8>)>,
pub cmdlines: Vec<(Vpid, Vec<u8>)>,
pub exes: Vec<(Vpid, Vec<u8>)>,
}
impl StateSnapshot {
pub fn write_to(&self, w: &mut impl Write) -> io::Result<()> {
w.write_all(&MAGIC)?;
w.write_all(&self.version.to_le_bytes())?;
self.cpu_feature_baseline.write_to(w)?;
self.entry_kind.write_to(w)?;
self.mem.write_to(w)?;
self.proctree.write_to(w)?;
self.netstack.write_to(w)?;
self.fdtable.write_to(w)?;
w.write_all(&(self.cwds.len() as u32).to_le_bytes())?;
for (vpid, path) in &self.cwds {
w.write_all(&vpid.to_le_bytes())?;
wblob(w, path)?;
}
w.write_all(&(self.cmdlines.len() as u32).to_le_bytes())?;
for (vpid, cmdline) in &self.cmdlines {
w.write_all(&vpid.to_le_bytes())?;
wblob(w, cmdline)?;
}
w.write_all(&(self.exes.len() as u32).to_le_bytes())?;
for (vpid, exe) in &self.exes {
w.write_all(&vpid.to_le_bytes())?;
wblob(w, exe)?;
}
Ok(())
}
pub fn read_from(r: &mut impl Read) -> io::Result<StateSnapshot> {
let magic = rb::<8, _>(r)?;
if magic != MAGIC {
return Err(err(format!(
"state-snapshot magic mismatch: got {magic:02x?}, expected {MAGIC:02x?}"
)));
}
let version = ru32(r)?;
if version != CURRENT_VERSION {
return Err(err(format!(
"state-snapshot version {version} != supported {CURRENT_VERSION} \
(hard refusal — no best-effort parse across a version skew)"
)));
}
let cpu_feature_baseline = CpuFeatures::read_from(r)?;
let entry_kind = SnapshotEntryKind::read_from(r)?;
let mem = MemImage::read_from(r)?;
let proctree = ProcTreeSnapshot::read_from(r)?;
let netstack = super::netstack::LoopSnapshot::read_from(r)?;
let fdtable = FdTableSnapshot::read_from(r)?;
let (n, hint) = read_count(r, 8)?;
let mut cwds = Vec::with_capacity(hint);
for _ in 0..n {
let vpid = ru32(r)?;
let path = rblob(r, "cwd")?;
cwds.push((vpid, path));
}
let (n, hint) = read_count(r, 8)?;
let mut cmdlines = Vec::with_capacity(hint);
for _ in 0..n {
let vpid = ru32(r)?;
let cmdline = rblob(r, "cmdline")?;
cmdlines.push((vpid, cmdline));
}
let (n, hint) = read_count(r, 8)?;
let mut exes = Vec::with_capacity(hint);
for _ in 0..n {
let vpid = ru32(r)?;
let exe = rblob(r, "exe")?;
exes.push((vpid, exe));
}
Ok(StateSnapshot {
version,
cpu_feature_baseline,
entry_kind,
mem,
proctree,
netstack,
fdtable,
cwds,
cmdlines,
exes,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn sample_thread(vtid: u32) -> ThreadRegs {
let mut gregs = [0i64; 18];
for (i, g) in gregs.iter_mut().enumerate() {
*g = (i as i64) * 0x1111 - 3; }
ThreadRegs {
vtid,
gregs,
fs: 0x7f00_0000_1234,
xsave: (0..512u32).map(|i| i as u8).collect(),
}
}
fn sample_proc(vpid: Vpid, state: ProcState, nthreads: u32) -> GuestProcRecord {
GuestProcRecord {
vpid,
parent_vpid: vpid.saturating_sub(1),
pgid: vpid + 100,
sid: vpid + 200,
threads: (0..nthreads)
.map(|t| sample_thread(vpid * 10 + t))
.collect(),
state,
}
}
fn sample_loop_netstack() -> crate::sentry_portable::netstack::LoopSnapshot {
use crate::sentry_portable::netstack::{Endpoint, LoopSnapshot, SockRecord, SockState};
let lo = |port: u16| Endpoint {
v6: false,
ip: {
let mut ip = [0u8; 16];
ip[..4].copy_from_slice(&[127, 0, 0, 1]);
ip
},
port,
};
LoopSnapshot {
next_id: 3,
next_ephemeral: 49160,
socks: vec![
SockRecord {
id: 1,
state: SockState::Established,
local: Some(lo(5432)),
remote: Some(lo(49152)),
peer: Some(2),
rx: vec![0xde, 0xad, 0xbe, 0xef],
rd_shut: false,
peer_wr_shut: false,
backlog: vec![],
},
SockRecord {
id: 2,
state: SockState::Established,
local: Some(lo(49152)),
remote: Some(lo(5432)),
peer: Some(1),
rx: vec![0x01, 0x02],
rd_shut: false,
peer_wr_shut: false,
backlog: vec![],
},
],
}
}
fn sample_snapshot() -> StateSnapshot {
let regions = vec![
VmaRecord {
va_start: 0x40_0000,
len: 0x2000,
prot: 0b101, file_off: 0,
},
VmaRecord {
va_start: 0x42_0000,
len: 0x1000,
prot: 0b011, file_off: 0x2000,
},
];
let thread = sample_thread(1);
StateSnapshot {
version: CURRENT_VERSION,
cpu_feature_baseline: CpuFeatures::from_bits(
CpuFeatures::SSE2 | CpuFeatures::AVX | CpuFeatures::AVX2,
),
entry_kind: SnapshotEntryKind::LiveTree,
mem: MemImage {
regions: regions.clone(),
threads: vec![thread.clone()],
procs: vec![
ProcMemImage {
vpid: 1,
blob_off: 0,
regions: regions.clone(),
threads: vec![thread],
},
ProcMemImage {
vpid: 2,
blob_off: 0x8000,
regions: vec![VmaRecord {
va_start: 0x50_0000,
len: 0x1000,
prot: 0b011,
file_off: 0,
}],
threads: vec![sample_thread(20)],
},
],
},
proctree: ProcTreeSnapshot {
procs: vec![
sample_proc(1, ProcState::Running, 2),
sample_proc(2, ProcState::Zombie(-9), 1),
sample_proc(3, ProcState::Reaped, 0),
],
},
netstack: sample_loop_netstack(),
fdtable: FdTableSnapshot {
per_vpid: vec![
(
1,
vec![
(
0,
FdTarget::DevNode {
kind: DevKind::Null,
flags: 0,
},
),
(
1,
FdTarget::RootRelFile {
path: b"/var/log/app.log".to_vec(),
offset: 4096,
flags: 0o2001, },
),
(3, FdTarget::LoopSocket(SocketHandle(42))),
(
8,
FdTarget::Epoll {
loop_watches: vec![EpollLoopWatch {
socket: SocketHandle(42),
events: 1,
data: 99,
}],
},
),
],
),
(
2,
vec![
(
4,
FdTarget::Pipe {
id: 0xfeed,
end: PipeEnd::Read,
buffered: vec![1, 2, 3, 4, 5],
flags: 0,
},
),
(
5,
FdTarget::HostEgressSocket {
proto: EgressProto::Tcp,
v6: false,
local_ip: {
let mut a = [0u8; 16];
a[..4].copy_from_slice(&[10, 0, 0, 2]);
a
},
local_port: 33000,
connected: true,
remote_ip: {
let mut a = [0u8; 16];
a[..4].copy_from_slice(&[93, 184, 216, 34]);
a
},
remote_port: 443,
},
),
],
),
],
},
cwds: vec![
(1, b"/".to_vec()),
(2, b"/srv/app".to_vec()),
(3, b"/tmp".to_vec()),
],
cmdlines: vec![
(1, b"app\0serve\0".to_vec()),
(2, b"worker\0".to_vec()),
(3, b"helper\0".to_vec()),
],
exes: vec![
(1, b"/usr/bin/app".to_vec()),
(2, b"/bin/busybox".to_vec()),
(3, b"/bin/sh".to_vec()),
],
}
}
macro_rules! assert_round_trip {
($val:expr, $ty:ty) => {{
let val = $val;
let mut buf = Vec::new();
val.write_to(&mut buf).expect("write_to");
let mut cur = std::io::Cursor::new(&buf[..]);
let back = <$ty>::read_from(&mut cur).expect("read_from");
assert_eq!(
cur.position() as usize,
buf.len(),
"trailing slack for {}",
stringify!($ty)
);
assert_eq!(val, back, "round-trip mismatch for {}", stringify!($ty));
}};
}
#[test]
fn thread_regs_round_trip() {
assert_round_trip!(sample_thread(7), ThreadRegs);
}
#[test]
fn guest_proc_record_round_trip() {
for st in [
ProcState::Running,
ProcState::Zombie(137),
ProcState::Reaped,
] {
assert_round_trip!(sample_proc(5, st, 3), GuestProcRecord);
}
}
#[test]
fn vma_record_round_trip_and_fixed_width() {
let v = VmaRecord {
va_start: 0xdead_beef,
len: 0x1_0000,
prot: 7,
file_off: 0x4000,
};
let mut buf = Vec::new();
v.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), VmaRecord::WIRE_LEN, "VmaRecord fixed wire width");
assert_round_trip!(v, VmaRecord);
}
#[test]
fn fd_target_round_trip_every_variant() {
let variants = [
FdTarget::LoopSocket(SocketHandle(0xabcd)),
FdTarget::RootRelFile {
path: b"/etc/hosts".to_vec(),
offset: 1234,
flags: 0,
},
FdTarget::HostEgressSocket {
proto: EgressProto::Udp,
v6: true,
local_ip: [1; 16],
local_port: 7,
connected: false,
remote_ip: [0; 16],
remote_port: 0,
},
FdTarget::DevNode {
kind: DevKind::Random,
flags: 0o4000,
},
FdTarget::Pipe {
id: 99,
end: PipeEnd::Write,
buffered: vec![9, 8, 7],
flags: 0o4001,
},
FdTarget::Epoll {
loop_watches: vec![EpollLoopWatch {
socket: SocketHandle(0xabcd),
events: 0x001,
data: 0xfeed_beef,
}],
},
];
for v in variants {
assert_round_trip!(v, FdTarget);
}
}
#[test]
fn proc_mem_image_round_trip() {
let image = sample_snapshot().mem.procs[1].clone();
assert_round_trip!(image, ProcMemImage);
}
#[test]
fn mem_image_round_trip() {
assert_round_trip!(sample_snapshot().mem, MemImage);
}
#[test]
fn mem_image_looks_up_per_vpid_records() {
let mem = sample_snapshot().mem;
let root = mem.proc_image(1).expect("vpid 1 image");
assert_eq!(root.blob_off, 0);
assert_eq!(root.regions, mem.regions.as_slice());
assert_eq!(root.threads, mem.threads.as_slice());
let child = mem.proc_image(2).expect("vpid 2 image");
assert_eq!(child.blob_off, 0x8000);
assert_eq!(child.regions[0].va_start, 0x50_0000);
assert!(mem.proc_image(99).is_none());
let legacy = MemImage {
regions: mem.regions.clone(),
threads: mem.threads.clone(),
procs: Vec::new(),
};
assert!(
legacy.proc_image(1).is_some(),
"vpid 1 falls back to the singleton projection"
);
assert!(legacy.proc_image(2).is_none());
}
#[test]
fn proctree_round_trip() {
assert_round_trip!(sample_snapshot().proctree, ProcTreeSnapshot);
}
#[test]
fn netstack_round_trip() {
assert_round_trip!(
sample_snapshot().netstack,
crate::sentry_portable::netstack::LoopSnapshot
);
}
#[test]
fn fdtable_round_trip() {
assert_round_trip!(sample_snapshot().fdtable, FdTableSnapshot);
}
#[test]
fn full_state_snapshot_round_trips() {
let snap = sample_snapshot();
let mut buf = Vec::new();
snap.write_to(&mut buf).unwrap();
let mut cur = Cursor::new(&buf);
let back = StateSnapshot::read_from(&mut cur).unwrap();
assert_eq!(cur.position() as usize, buf.len(), "trailing slack");
assert_eq!(snap, back);
}
#[test]
fn cpu_features_satisfied_by_superset_only() {
let want = CpuFeatures::from_bits(CpuFeatures::SSE2 | CpuFeatures::AVX2);
let host_super =
CpuFeatures::from_bits(CpuFeatures::SSE2 | CpuFeatures::AVX | CpuFeatures::AVX2);
let host_missing = CpuFeatures::from_bits(CpuFeatures::SSE2); assert!(want.satisfied_by(&host_super), "superset host satisfies");
assert!(want.satisfied_by(&want), "equal host satisfies");
assert!(
!want.satisfied_by(&host_missing),
"host missing AVX2 must NOT satisfy an AVX2 snapshot"
);
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
let cur = CpuFeatures::current();
assert!(
cur.bits() & CpuFeatures::SSE2 != 0,
"x86_64 baseline has sse2"
);
}
}
fn golden_snapshot() -> StateSnapshot {
let regions = vec![VmaRecord {
va_start: 0x400000,
len: 0x1000,
prot: 5,
file_off: 0,
}];
StateSnapshot {
version: CURRENT_VERSION,
cpu_feature_baseline: CpuFeatures::from_bits(CpuFeatures::SSE2),
entry_kind: SnapshotEntryKind::WarmZygote,
mem: MemImage {
regions: regions.clone(),
threads: vec![],
procs: vec![ProcMemImage {
vpid: 1,
blob_off: 0,
regions,
threads: vec![],
}],
},
proctree: ProcTreeSnapshot {
procs: vec![GuestProcRecord {
vpid: 1,
parent_vpid: 0,
pgid: 1,
sid: 1,
threads: vec![],
state: ProcState::Running,
}],
},
netstack: crate::sentry_portable::netstack::LoopSnapshot::default(),
fdtable: FdTableSnapshot {
per_vpid: vec![(
1,
vec![(
0,
FdTarget::DevNode {
kind: DevKind::Zero,
flags: 0,
},
)],
)],
},
cwds: vec![(1, b"/".to_vec())],
cmdlines: vec![(1, b"init\0".to_vec())],
exes: vec![(1, b"/sbin/init".to_vec())],
}
}
#[test]
fn golden_fixture_is_byte_stable() {
let mut buf = Vec::new();
golden_snapshot().write_to(&mut buf).unwrap();
let expected: Vec<u8> = vec![
83, 77, 83, 84, 83, 78, 80, 49, 7, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
0, 64, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 64, 0, 0, 0,
0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 83, 77, 76, 79, 79,
80, 48, 49, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0,
0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 47, 1, 0, 0, 0, 1,
0, 0, 0, 5, 0, 0, 0, 105, 110, 105, 116, 0, 1, 0, 0, 0, 1, 0, 0, 0, 10, 0, 0, 0, 47,
115, 98, 105, 110, 47, 105, 110, 105, 116,
];
assert_eq!(
buf, expected,
"wire format drifted (or golden builder changed) — \
update the fixture deliberately, never blindly"
);
let back = StateSnapshot::read_from(&mut Cursor::new(&expected)).unwrap();
assert_eq!(golden_snapshot(), back);
}
#[test]
fn version_skew_is_hard_refused() {
let mut buf = Vec::new();
sample_snapshot().write_to(&mut buf).unwrap();
buf[8] = 0xff;
buf[9] = 0xff;
let e = StateSnapshot::read_from(&mut Cursor::new(&buf)).unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::InvalidData);
assert!(
e.to_string().contains("version"),
"error should name the version skew: {e}"
);
}
#[test]
fn bad_magic_is_refused() {
let mut buf = Vec::new();
sample_snapshot().write_to(&mut buf).unwrap();
buf[0] ^= 0xff; let e = StateSnapshot::read_from(&mut Cursor::new(&buf)).unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::InvalidData);
assert!(
e.to_string().contains("magic"),
"error should name magic: {e}"
);
}
#[test]
fn hostile_blob_length_is_refused_not_allocated() {
let mut buf = Vec::new();
buf.extend_from_slice(&7u32.to_le_bytes()); buf.extend_from_slice(&[0u8; 18 * 8]); buf.extend_from_slice(&0u64.to_le_bytes()); buf.extend_from_slice(&u32::MAX.to_le_bytes()); let e = ThreadRegs::read_from(&mut Cursor::new(&buf)).unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn hostile_count_does_not_overallocate() {
let mut buf = Vec::new();
buf.extend_from_slice(&u32::MAX.to_le_bytes()); let e = ProcTreeSnapshot::read_from(&mut Cursor::new(&buf)).unwrap_err();
assert!(
matches!(
e.kind(),
io::ErrorKind::UnexpectedEof | io::ErrorKind::InvalidData
),
"unexpected kind {:?}",
e.kind()
);
}
#[test]
fn truncated_full_snapshot_errors_at_every_prefix() {
let mut buf = Vec::new();
sample_snapshot().write_to(&mut buf).unwrap();
for cut in 0..buf.len() {
let r = StateSnapshot::read_from(&mut Cursor::new(&buf[..cut].to_vec()));
assert!(r.is_err(), "prefix len {cut} should error, not parse");
}
}
}