use std::{
cell::Cell,
fmt,
fs::File,
os::{
fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd},
unix::fs::FileExt,
},
sync::{OnceLock, RwLock},
};
use libc::c_long;
use libseccomp::{ScmpArch, ScmpNotifResp};
use memchr::arch::all::is_equal;
use nix::{errno::Errno, fcntl::SealFlag, unistd::Pid};
use serde::{Serialize, Serializer};
use crate::{
config::{KCOV_HEART_BEAT, SAFE_MFD_FLAGS},
cookie::{safe_ftruncate, safe_memfd_create},
err::err2no,
fd::{fd_inode, seal_memfd},
hash::SydHashMap,
ioctl::{Ioctl, IoctlMap},
kcov::{
clear_tls_sink, get_kcov_tid, get_tls_sink, remove_kcov_tid, set_kcov_tid, set_tls_sink,
Kcov, KcovId, TraceMode,
},
lookup::MaybeFd,
proc::proc_kcov_read_id,
req::UNotifyEventRequest,
};
extern "C" {
fn syd_kcov_set_syscall(nr: c_long);
}
thread_local! {
static TLS_SYS: Cell<c_long> = const { Cell::new(-1) };
}
thread_local! {
static TLS_CTX: Cell<(i32, u64, i32)> = const { Cell::new((-1, 0, -1)) };
}
thread_local! {
static TLS_REC: Cell<bool> = const { Cell::new(false) };
}
pub(crate) struct KcovSnap {
prev: bool,
}
impl KcovSnap {
pub(crate) fn new() -> Self {
let prev = TLS_REC.with(|c| {
let p = c.get();
c.set(true);
p
});
Self { prev }
}
}
impl Drop for KcovSnap {
fn drop(&mut self) {
TLS_REC.with(|c| c.set(self.prev));
}
}
pub(crate) fn kcov_set_syscall(nr: c_long) {
TLS_SYS.with(|c| c.set(nr));
unsafe { syd_kcov_set_syscall(nr) };
}
fn mix_syscall(mut pc: u64) -> u64 {
let nr = TLS_SYS.with(|c| c.get());
if nr > 0 {
pc ^= (nr as u64).wrapping_mul(0x517c_c1b7_2722_0a95);
}
pc
}
const fn to_canon_pc(v: u64) -> u64 {
let v = v & !0xFu64;
if cfg!(target_pointer_width = "64") {
0xFFFF_FFFF_8000_0000u64 | (v & 0x3FFF_FFF0u64)
} else {
let x = (v as u32) & 0x0FFF_FFF0u32;
(0x8000_0000u32 | x) as u64
}
}
fn payload_cap_records(ctx: &KcovCtx) -> usize {
match ctx.mode {
Some(TraceMode::Pc) => ctx.words.saturating_sub(1),
Some(TraceMode::Cmp) => (ctx.words.saturating_sub(1)) / 4,
None => 0,
}
}
fn kcov_cmp_type(size_bytes: u8, is_const: bool) -> u64 {
let size_code = match size_bytes {
1 => 0u64,
2 => 2u64,
4 => 4u64,
8 => 6u64,
_ => 6u64,
};
size_code | u64::from(is_const)
}
fn read_header_ne(ctx: &KcovCtx) -> Result<u64, Errno> {
let mut hdr = [0u8; 8];
if ctx.syd_fd.read_at(&mut hdr, 0).is_err() {
return Err(Errno::EIO);
}
Ok(u64::from_ne_bytes(hdr))
}
fn write_header_ne(ctx: &KcovCtx, val: u64) -> Result<(), Errno> {
let bytes = val.to_ne_bytes();
ctx.syd_fd
.write_all_at(&bytes, 0)
.map_err(|err| err2no(&err))
}
fn write_payload_word(ctx: &KcovCtx, idx: usize, val: u64) -> Result<(), Errno> {
let off = ((1 + idx) * 8) as u64;
let bytes = val.to_ne_bytes();
ctx.syd_fd
.write_all_at(&bytes, off)
.map_err(|err| err2no(&err))
}
fn zero_memfd(ctx: &mut KcovCtx) -> Result<(), Errno> {
if ctx.words == 0 {
return Err(Errno::EINVAL);
}
let need = ctx.words * 8;
ensure_len(&mut ctx.scratch, need);
for b in &mut ctx.scratch[..need] {
*b = 0;
}
ctx.syd_fd
.write_all_at(&ctx.scratch[..need], 0)
.map_err(|err| err2no(&err))?;
ctx.syd_fd.sync_data().or(Err(Errno::EIO))
}
fn live_update_pc_clamped(ctx: &KcovCtx, pc: u64) {
if ctx.mode != Some(TraceMode::Pc) || ctx.words <= 1 {
return;
}
let cap = payload_cap_records(ctx);
if cap == 0 {
return;
}
let mut cnt = match read_header_ne(ctx) {
Ok(n) => n as usize,
Err(_) => return,
};
if cnt >= cap {
if cnt != cap {
let _ = write_header_ne(ctx, cap as u64);
}
return;
}
let _ = write_payload_word(ctx, cnt, pc);
cnt += 1;
let _ = write_header_ne(ctx, cnt as u64);
}
pub(crate) struct KcovCtx {
pub(crate) id: KcovId,
pub(crate) syd_fd: File,
pub(crate) words: usize,
pub(crate) mode: Option<TraceMode>,
pub(crate) scratch: Vec<u8>,
}
static KCOV_REG: OnceLock<RwLock<SydHashMap<KcovId, KcovCtx>>> = OnceLock::new();
pub(crate) fn kcov_reg() -> &'static RwLock<SydHashMap<KcovId, KcovCtx>> {
KCOV_REG.get_or_init(|| RwLock::new(SydHashMap::default()))
}
static KCOV_MGR: OnceLock<Kcov> = OnceLock::new();
pub(crate) fn kcov_mgr() -> &'static Kcov {
KCOV_MGR.get_or_init(Kcov::new)
}
#[allow(clippy::cognitive_complexity)]
pub(crate) fn kcov_open(_tid: Pid) -> Result<MaybeFd, Errno> {
let memfd = safe_memfd_create(c"syd-kcov", *SAFE_MFD_FLAGS)?.into_raw_fd();
let memfd_own = unsafe { OwnedFd::from_raw_fd(memfd) };
let kcov_id = fd_inode(&memfd_own)?;
kcov_mgr().open(kcov_id)?;
{
let kcov_id = KcovId(kcov_id);
let _snap = KcovSnap::new(); let mut map = kcov_reg().write().unwrap_or_else(|e| e.into_inner());
map.insert(
kcov_id,
KcovCtx {
id: kcov_id,
syd_fd: memfd_own.into(),
words: 0,
mode: None,
scratch: Vec::new(),
},
);
}
Ok(memfd.into())
}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
struct KcovRemoteArg {
trace_mode: u32,
area_size: u32,
num_handles: u32,
common_handle: u64,
}
#[allow(clippy::cognitive_complexity)]
pub(crate) fn kcov_ioctl(request: &UNotifyEventRequest) -> Result<ScmpNotifResp, Errno> {
let tid = request.scmpreq.pid();
let fd = match RawFd::try_from(request.scmpreq.data.args[0]) {
Ok(fd) if fd >= 0 => fd,
_ => return Err(Errno::EBADF),
};
let kcov_id = match proc_kcov_read_id(tid, fd) {
Ok(id) => id,
Err(_) => return Err(Errno::ENOTTY),
};
#[allow(clippy::cast_possible_truncation)]
let kcov_req = Ioctl::from(request.scmpreq.data.args[1] as u32);
let kcov_arg = request.scmpreq.data.args[2];
let kcov_cmd = match KcovIoctl::try_from((kcov_req, request.scmpreq.data.arch)) {
Ok(cmd) => cmd,
Err(_) => return Err(Errno::ENOTTY),
};
#[allow(clippy::cast_possible_truncation)]
let result = match kcov_cmd {
KcovIoctl::InitTrace => {
let words = kcov_arg;
kcov_mgr().init_trace(kcov_id, words)?;
let _snap = KcovSnap::new(); let mut map = kcov_reg().write().unwrap_or_else(|e| e.into_inner());
let ctx = match map.get_mut(&kcov_id) {
Some(ctx) => ctx,
None => return Err(Errno::ENOTTY),
};
ctx.words = words as usize;
safe_ftruncate(&ctx.syd_fd, (ctx.words * 8) as i64)?;
let flags = SealFlag::F_SEAL_SEAL | SealFlag::F_SEAL_SHRINK | SealFlag::F_SEAL_GROW;
seal_memfd(&ctx.syd_fd, flags)?;
zero_memfd(ctx)?;
Ok(ok0(request))
}
KcovIoctl::Enable => {
set_kcov_tid(tid, kcov_id, false);
let mode = match kcov_arg {
0 => TraceMode::Pc,
1 => TraceMode::Cmp,
_ => return Err(Errno::EINVAL),
};
let id = {
let map = kcov_reg().read().unwrap_or_else(|e| e.into_inner());
let ctx = map.get(&kcov_id).ok_or(Errno::ENOTTY)?;
if ctx.words == 0 {
return Err(Errno::EINVAL);
}
ctx.id
};
kcov_mgr().enable(id, mode)?;
{
let _snap = KcovSnap::new(); let mut map = kcov_reg().write().unwrap_or_else(|e| e.into_inner());
let ctx = map.get_mut(&id).ok_or(Errno::ENOTTY)?;
ctx.mode = Some(mode);
}
Ok(ok0(request))
}
KcovIoctl::RemoteEnable => {
let mut arg = KcovRemoteArg::default();
let buf = unsafe {
std::slice::from_raw_parts_mut(
&raw mut arg as *mut u8,
std::mem::size_of::<KcovRemoteArg>(),
)
};
let n = request.read_mem(buf, kcov_arg, buf.len())?;
if n != buf.len() {
return Err(Errno::EFAULT);
}
let mode = match arg.trace_mode {
0 => TraceMode::Pc,
1 => TraceMode::Cmp,
_ => return Err(Errno::EINVAL),
};
let id = {
let map = kcov_reg().read().unwrap_or_else(|e| e.into_inner());
let ctx = map.get(&kcov_id).ok_or(Errno::ENOTTY)?;
if ctx.words == 0 {
return Err(Errno::EINVAL);
}
ctx.id
};
set_kcov_tid(tid, id, true);
kcov_mgr().enable(id, mode)?;
{
let _snap = KcovSnap::new(); let mut map = kcov_reg().write().unwrap_or_else(|e| e.into_inner());
let ctx = map.get_mut(&id).ok_or(Errno::ENOTTY)?;
ctx.mode = Some(mode);
}
Ok(ok0(request))
}
KcovIoctl::Disable => {
remove_kcov_tid(tid);
let id = {
let map = kcov_reg().read().unwrap_or_else(|e| e.into_inner());
let ctx = map.get(&kcov_id).ok_or(Errno::ENOTTY)?;
ctx.id
};
kcov_mgr().disable(id)?;
Ok(ok0(request))
}
KcovIoctl::ResetTrace => {
let _snap = KcovSnap::new(); let mut map = kcov_reg().write().unwrap_or_else(|e| e.into_inner());
let ctx = map.get_mut(&kcov_id).ok_or(Errno::ENOTTY)?;
zero_memfd(ctx)?;
if let Some(mode) = ctx.mode {
if let Ok(file) = ctx.syd_fd.try_clone() {
emit_heartbeats(&file, ctx.words, mode);
}
}
Ok(ok0(request))
}
_ => Err(Errno::ENOTTY),
};
result
}
pub(crate) fn kcov_enter_for(tid: Pid) -> Result<(), Errno> {
let kcov_id = match get_kcov_tid(tid) {
Some(id) => {
set_tls_sink(id);
id
}
None => {
clear_tls_sink();
return Ok(());
}
};
let map = kcov_reg().read().unwrap_or_else(|e| e.into_inner());
let ctx = match map.get(&kcov_id) {
Some(ctx) => ctx,
None => {
TLS_CTX.with(|c| c.set((-1, 0, -1)));
return Ok(());
}
};
if ctx.mode.is_none() {
TLS_CTX.with(|c| c.set((-1, 0, -1)));
return Ok(());
}
let cached_fd = ctx.syd_fd.as_raw_fd();
let cached_words = ctx.words as u64;
let cached_mode = match ctx.mode {
Some(TraceMode::Pc) => 0,
Some(TraceMode::Cmp) => 1,
None => -1,
};
TLS_CTX.with(|c| c.set((cached_fd, cached_words, cached_mode)));
if ctx.words > 0 {
let file_clone = match ctx.syd_fd.try_clone() {
Ok(f) => f,
Err(_) => return Ok(()),
};
let words = ctx.words;
let mode = ctx.mode.unwrap();
drop(map);
emit_heartbeats(&file_clone, words, mode);
}
Ok(())
}
pub(crate) fn kcov_exit_for(_tid: Pid) -> Result<(), Errno> {
let kcov_id = match get_tls_sink() {
Some(id) => id,
None => return Ok(()),
};
let map = kcov_reg().read().unwrap_or_else(|e| e.into_inner());
let ctx = match map.get(&kcov_id) {
Some(ctx) => ctx,
None => return Ok(()),
};
let cnt = match read_header_ne(ctx) {
Ok(n) => n,
Err(_) => return Ok(()),
};
if cnt > 0 {
let _ = ctx.syd_fd.sync_data();
}
Ok(())
}
fn emit_heartbeats(file: &File, words: usize, mode: TraceMode) {
match mode {
TraceMode::Pc => {
if words > 1 {
let pc = to_canon_pc(mix_syscall(KCOV_HEART_BEAT)).to_ne_bytes();
let hdr = 1u64.to_ne_bytes();
if file.write_all_at(&pc, 8).is_err() {
return;
}
if file.write_all_at(&hdr, 0).is_err() {
return;
}
let _ = file.sync_data();
}
}
TraceMode::Cmp => {
if words > 4 {
let ty = kcov_cmp_type(8, false).to_ne_bytes();
let a = 1u64.to_ne_bytes();
let b = 0u64.to_ne_bytes();
let ip = to_canon_pc(mix_syscall(KCOV_HEART_BEAT)).to_ne_bytes();
let hdr = 1u64.to_ne_bytes();
if file.write_all_at(&ty, 8).is_err()
|| file.write_all_at(&a, 16).is_err()
|| file.write_all_at(&b, 24).is_err()
|| file.write_all_at(&ip, 32).is_err()
{
return;
}
if file.write_all_at(&hdr, 0).is_err() {
return;
}
let _ = file.sync_data();
}
}
}
}
pub(crate) fn kcov_attach(pid: Pid) {
if let Some(id) = get_kcov_tid(pid) {
set_tls_sink(id);
let _ = kcov_enter_for(pid);
} else {
clear_tls_sink();
}
}
fn ok0(req: &UNotifyEventRequest) -> ScmpNotifResp {
ScmpNotifResp::new(req.scmpreq.id, 0, 0, 0)
}
fn ensure_len(vec: &mut Vec<u8>, need: usize) {
if vec.len() < need {
vec.resize(need, 0);
}
}
#[repr(C)]
pub(crate) struct kcov_ctx {
pub(crate) fd: RawFd,
pub(crate) words: u64,
pub(crate) mode: i32,
}
#[no_mangle]
pub extern "C" fn syd_kcov_get_ctx(out_ctx: *mut kcov_ctx) -> bool {
if out_ctx.is_null() {
return false;
}
let (fd, words, mode) = TLS_CTX.with(|c| c.get());
if fd < 0 || words == 0 {
return false;
}
unsafe {
(*out_ctx).fd = fd;
(*out_ctx).words = words;
(*out_ctx).mode = mode;
}
true
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum KcovIoctl {
InitTrace,
ResetTrace,
Enable,
RemoteEnable,
UniqueEnable,
Disable,
}
impl TryFrom<(Ioctl, ScmpArch)> for KcovIoctl {
type Error = Errno;
fn try_from(value: (Ioctl, ScmpArch)) -> Result<Self, Errno> {
let (val, arch) = value;
let map = IoctlMap::new(None, true);
let names = map.get_names(val, arch)?.ok_or(Errno::ENOTTY)?;
for name in names {
let name = name.as_bytes();
if is_equal(name, b"KCOV_INIT_TRACE") {
return Ok(Self::InitTrace);
} else if is_equal(name, b"KCOV_RESET_TRACE") {
return Ok(Self::ResetTrace);
} else if is_equal(name, b"KCOV_ENABLE") {
return Ok(Self::Enable);
} else if is_equal(name, b"KCOV_REMOTE_ENABLE") {
return Ok(Self::RemoteEnable);
} else if is_equal(name, b"KCOV_UNIQUE_ENABLE") {
return Ok(Self::UniqueEnable);
} else if is_equal(name, b"KCOV_DISABLE") {
return Ok(Self::Disable);
}
}
Err(Errno::ENOTTY)
}
}
impl fmt::Display for KcovIoctl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::InitTrace => "kcov_init_trace",
Self::ResetTrace => "kcov_reset_trace",
Self::Enable => "kcov_enable",
Self::RemoteEnable => "kcov_remote_enable",
Self::UniqueEnable => "kcov_unique_enable",
Self::Disable => "kcov_disable",
};
write!(f, "{name}")
}
}
impl Serialize for KcovIoctl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[inline(never)]
pub(crate) fn record_pc(pc: u64) -> Result<(), Errno> {
if TLS_REC.with(|c| {
if c.get() {
true
} else {
c.set(true);
false
}
}) {
return Ok(());
}
let pc = to_canon_pc(pc);
if let Some(id) = get_tls_sink() {
let map = kcov_reg().read().unwrap_or_else(|e| e.into_inner());
if let Some(ctx) = map.get(&id) {
live_update_pc_clamped(ctx, pc);
}
}
TLS_REC.with(|c| c.set(false));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_canon_pc_alignment_1() {
let pc = to_canon_pc(0x10);
assert_eq!(pc & 0xF, 0, "must be 16-byte aligned");
}
#[test]
fn test_to_canon_pc_alignment_2() {
let pc = to_canon_pc(0x1F);
assert_eq!(pc & 0xF, 0, "unaligned input must be rounded down");
}
#[test]
fn test_to_canon_pc_zero_1() {
let pc = to_canon_pc(0);
assert_eq!(pc & 0xF, 0);
}
#[test]
fn test_to_canon_pc_kernel_range_1() {
if cfg!(target_pointer_width = "64") {
let pc = to_canon_pc(0x100);
assert!(pc >= 0xFFFF_FFFF_8000_0000u64);
}
}
#[test]
fn test_kcov_cmp_type_size1_const_1() {
assert_eq!(kcov_cmp_type(1, true), 0u64 | 1u64);
}
#[test]
fn test_kcov_cmp_type_size1_not_const_1() {
assert_eq!(kcov_cmp_type(1, false), 0u64);
}
#[test]
fn test_kcov_cmp_type_size2_const_1() {
assert_eq!(kcov_cmp_type(2, true), 2u64 | 1u64);
}
#[test]
fn test_kcov_cmp_type_size4_1() {
assert_eq!(kcov_cmp_type(4, false), 4u64);
}
#[test]
fn test_kcov_cmp_type_size8_1() {
assert_eq!(kcov_cmp_type(8, false), 6u64);
}
#[test]
fn test_kcov_cmp_type_unknown_size_1() {
assert_eq!(kcov_cmp_type(16, false), 6u64);
}
#[test]
fn test_kcov_cmp_type_const_bit_1() {
let with_const = kcov_cmp_type(8, true);
let without_const = kcov_cmp_type(8, false);
assert_eq!(with_const, without_const | 1);
}
}