// Syd: rock-solid application kernel // src/kcov/abi.rs — KCOV ABI handlers // // Copyright (c) 2025 Ali Polatel // SPDX-License-Identifier: GPL-3.0 use std::{ fmt, fs::File, marker::PhantomData, os::{ fd::{FromRawFd, IntoRawFd, OwnedFd, RawFd}, unix::fs::FileExt, }, sync::{OnceLock, RwLock}, }; use libc::pid_t; 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::SAFE_MFD_FLAGS, cookie::safe_ftruncate, err::err2no, error, fs::{create_memfd, seal_memfd, MaybeFd}, hash::SydHashMap, hook::UNotifyEventRequest, ioctl::{ioctl_names_get, Ioctl}, kcov::{Kcov, KcovId, TlsSink, TraceMode, TLS_SINK}, notice, proc::proc_kcov_readid, }; /// Internal: compute payload capacity (in records) for the given context/mode. #[inline] 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, } } /// Internal: read cover[0] (native-endian u64) from the memfd. #[inline] fn read_header_ne(ctx: &KcovCtx) -> Result { 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)) } /// Internal: write cover[0] (native-endian u64) to the memfd. #[inline] fn write_header_ne(ctx: &KcovCtx, val: u64) -> Result<(), Errno> { let bytes = val.to_ne_bytes(); if let Err(error) = ctx.syd_fd.write_all_at(&bytes, 0) { let errno = err2no(&error); error!("ctx" : "kcov", "op" : "write_header", "id" : ctx.id.0, "val" : val, "err" : errno as i32, "msg" : format!("KCOV:{} header write val:{val} failed: {errno}", ctx.id.0)); return Err(Errno::EIO); } Ok(()) } /// Internal: write a single payload u64 at record index `idx` (0-based). #[inline] fn write_payload_word(ctx: &KcovCtx, idx: usize, val: u64) -> Result<(), Errno> { // Payload starts at word 1 → byte offset = (1 + idx) * 8. let off = ((1 + idx) * 8) as u64; let bytes = val.to_ne_bytes(); if let Err(error) = ctx.syd_fd.write_all_at(&bytes, off) { let errno = err2no(&error); error!("ctx" : "kcov", "op" : "write_payload", "id" : ctx.id.0, "idx" : idx, "val" : val, "err" : errno as i32, "msg" : format!("KCOV:{} payload write idx:{idx} val:{val} failed: {errno}", ctx.id.0)); return Err(Errno::EIO); } Ok(()) } // Internal: zero the live memfd header+payload strictly within `words`. #[inline] 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) .or(Err(Errno::EIO)) } /// Internal: best-effort live memfd update for a PC record with capacity clamp. /// /// If full, clamps header to capacity and performs no payload write. #[inline] fn live_update_pc_clamped(ctx: &mut KcovCtx, pc: u64) { // Only when in PC mode with a valid area. 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 { // Clamp header if it drifted past cap; ignore errors. if cnt != cap { let _ = write_header_ne(ctx, cap as u64); } return; } // Write payload at current index and bump header; ignore I/O errors. let _ = write_payload_word(ctx, cnt, pc); cnt += 1; let _ = write_header_ne(ctx, cnt as u64); } /// Internal: best-effort live memfd update for a CMP record with capacity clamp. /// /// If full, clamps header to capacity and performs no payload write. #[inline] fn live_update_cmp_clamped( ctx: &mut KcovCtx, size_bytes: u8, is_const: bool, a: u64, b: u64, ip: u64, ) { // Only when in CMP mode with a valid area. if ctx.mode != Some(TraceMode::Cmp) || ctx.words <= 4 { return; } let payload_words = ctx.words.saturating_sub(1); let cap_records = payload_words / 4; if cap_records == 0 { return; } // Read current count from memfd header in native-endian. let mut cnt = match read_header_ne(ctx) { Ok(n) => n as usize, Err(_) => return, }; if cnt >= cap_records { // Clamp header if it drifted past cap; ignore errors. if cnt != cap_records { let _ = write_header_ne(ctx, cap_records as u64); } return; } // Compute per-record base index (4 words per record). let base = cnt.saturating_mul(4); // Pack comparison type: size in next byte, const in bit 0. let ty = (u64::from(size_bytes) << 8) | u64::from(is_const); // Write payload (ty, a, b, ip), then bump header; ignore I/O errors. let _ = write_payload_word(ctx, base, ty); let _ = write_payload_word(ctx, base + 1, a); let _ = write_payload_word(ctx, base + 2, b); let _ = write_payload_word(ctx, base + 3, ip); cnt += 1; let _ = write_header_ne(ctx, cnt as u64); } // Read `struct kcov_remote_arg` header (fixed 24 bytes) and optionally N u64 handles. // Layout is arch-stable due to `__aligned_u64` in uapi. #[repr(C)] #[derive(Clone, Copy)] pub(crate) struct KCovRemoteHdr { pub(crate) trace_mode: u32, // 0=PC, 1=CMP pub(crate) area_size: u32, // words pub(crate) num_handles: u32, pub(crate) _pad: u32, // keep next 8B-aligned pub(crate) common_handle: u64, } /// Per-TID kcov context. pub(crate) struct KcovCtx { pub(crate) id: KcovId, pub(crate) syd_fd: File, pub(crate) words: usize, pub(crate) mode: Option, pub(crate) scratch: Vec, } // // Singletons // static KCOV_REG: OnceLock>> = OnceLock::new(); fn reg() -> &'static RwLock> { KCOV_REG.get_or_init(|| RwLock::new(SydHashMap::default())) } /// Map owner (enabling) TID -> KcovId. static KCOV_OWNER_REG: OnceLock>> = OnceLock::new(); fn owner_reg() -> &'static RwLock> { KCOV_OWNER_REG.get_or_init(|| RwLock::new(SydHashMap::default())) } /// Check that enabling `id` for `owner_tid` would not violate the /// single-kcov-per-task rule. Does not mutate the map. #[inline] fn owner_check(owner_tid: pid_t, id: KcovId) -> Result<(), Errno> { let map = owner_reg().read().unwrap_or_else(|e| e.into_inner()); if matches!(map.get(&owner_tid), Some(cur) if *cur != id) { // Task already owns another KCOV instance. return Err(Errno::EBUSY); } Ok(()) } /// Bind `owner_tid` to `id` after a successful transition to Enabled. #[inline] fn owner_bind(owner_tid: pid_t, id: KcovId) { let mut map = owner_reg().write().unwrap_or_else(|e| e.into_inner()); map.insert(owner_tid, id); } // Rebind exact owner TID mapping (old -> new) for this KCOV id. #[inline] fn owner_rebind_tid(old_tid: pid_t, new_tid: pid_t, id: KcovId) { let mut map = owner_reg().write().unwrap_or_else(|e| e.into_inner()); if let Some(cur) = map.get(&old_tid).copied() { if cur == id { map.remove(&old_tid); } } // Install new owner tid. map.insert(new_tid, id); } /* #[inline] fn owner_set(owner_tid: pid_t, id: KcovId) { let mut map = owner_reg().write().unwrap_or_else(|e| e.into_inner()); map.insert(owner_tid, id); } */ #[inline] fn owner_clear(owner_tid: pid_t) { let mut map = owner_reg().write().unwrap_or_else(|e| e.into_inner()); map.remove(&owner_tid); } static KCOV_MGR: OnceLock = OnceLock::new(); fn mgr() -> &'static Kcov { KCOV_MGR.get_or_init(Kcov::new) } // // Public API // /// RAII guard that arms KCOV for the current worker. pub struct KcovScope { prev: Option, _nosend: PhantomData<*mut ()>, // !Send + !Sync } impl KcovScope { fn new(id: KcovId) -> Self { let prev = TLS_SINK.with(|tls| { let mut guard = tls.lock().unwrap_or_else(|e| e.into_inner()); let prev = *guard; *guard = Some(TlsSink { id }); prev }); Self { prev, _nosend: PhantomData, } } } impl Drop for KcovScope { fn drop(&mut self) { TLS_SINK.with(|tls| { let mut guard = tls.lock().unwrap_or_else(|e| e.into_inner()); *guard = self.prev; }); } } /// Create a named memfd for kcov, register per-TID context. #[allow(clippy::cognitive_complexity)] pub(crate) fn kcov_open(tid: Pid) -> Result { let tid = tid.as_raw(); notice!("ctx": "kcov", "op": "open", "pid": tid, "msg": format!("open /dev/kcov request by tid {tid}")); // Allocate ID first so we can encode in memfd name. let kcov_id = mgr().open()?; // Create memfd with Kcov ID in name. let name = format!("syd-kcov:{}", kcov_id.0); let mut name_c = name.clone().into_bytes(); name_c.push(0); let memfd = create_memfd(&name_c, *SAFE_MFD_FLAGS)?.into_raw_fd(); // SAFETY: seccomp addfd creates a duplicate. let memfd_own = unsafe { OwnedFd::from_raw_fd(memfd) }; // Register per-TID context (disabled until KCOV_ENABLE). { let mut map = 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(), }, ); } notice!("ctx": "kcov", "op": "open", "pid": tid, "name": &name, "msg": format!("open /dev/kcov returned !memfd:{name} to tid {tid}")); // Hand the original memfd back to caller. Ok(memfd.into()) } /// Emulate kcov ioctls on our memfd, identified by fd-name. #[allow(clippy::cognitive_complexity)] pub(crate) fn kcov_ioctl(request: &UNotifyEventRequest) -> Result { let tid = request.scmpreq.pid().as_raw(); let fd = match RawFd::try_from(request.scmpreq.data.args[0]) { Ok(fd) if fd >= 0 => fd, _ => return Err(Errno::EBADF), }; // Resolve the KcovId from the memfd name. let kcov_id = proc_kcov_readid(tid, fd).ok_or(Errno::ENOTTY)?; // Look up per-TID context, fallback to procfs readlink. let mut write_map = reg().write().unwrap_or_else(|e| e.into_inner()); let ctx = write_map.get_mut(&kcov_id).ok_or(Errno::ENOTTY)?; // Decode ioctl by NAME (arch-safe). let kcov_cmd = request.scmpreq.data.args[1] as Ioctl; let kcov_arg = request.scmpreq.data.args[2]; let kcov_cmd = KcovIoctl::try_from((kcov_cmd, request.scmpreq.data.arch))?; notice!("ctx": "kcov", "op": "ioctl", "pid": tid, "cmd": kcov_cmd, "fd": fd, "msg": format!("ioctl {kcov_cmd} request by tid {tid} for fd {fd}")); #[allow(clippy::cast_possible_truncation)] match kcov_cmd { KcovIoctl::InitTrace => { let words = kcov_arg; if words < 2 { return Err(Errno::EINVAL); } mgr().init_trace(ctx.id, words)?; // Track/resize our memfd view to match `words`. ctx.words = words as usize; if safe_ftruncate(&ctx.syd_fd, (ctx.words * 8) as i64).is_err() { return Err(Errno::EIO); } if seal_memfd(&ctx.syd_fd, SealFlag::F_SEAL_SHRINK | SealFlag::F_SEAL_GROW).is_err() { return Err(Errno::EIO); } // Zero the file (header+payload) strictly within words. zero_memfd(ctx)?; Ok(ok0(request)) } KcovIoctl::ResetTrace => { // Validate: Area must be initialized, // and ioctl arg must be 0. if ctx.words == 0 || kcov_arg != 0 { return Err(Errno::EINVAL); } // Header-only reset: // Kernel clears cover[0] and leaves payload unspecified. We follow // that to avoid O(n) zeroing on large areas. The live writer // derives the next index from the header and will overwrite payload // entries as it records. if write_header_ne(ctx, 0).is_err() { return Err(Errno::EIO); } // TLS remains armed; recording resumes at index 0. Ok(ok0(request)) } KcovIoctl::Enable | KcovIoctl::UniqueEnable => { let mode = match kcov_arg { 0 => TraceMode::Pc, 1 => TraceMode::Cmp, _ => return Err(Errno::EINVAL), }; if ctx.words == 0 { return Err(Errno::EINVAL); } let tid = request.scmpreq.pid().as_raw(); // Enforce single-kcov-per-task: owner_check(tid, ctx.id)?; // Transition to Enabled (owner recorded in the core). mgr().enable(ctx.id, mode, tid)?; // Publish the owner mapping only after enable. owner_bind(tid, ctx.id); ctx.mode = Some(mode); // Linux expects userspace to reset cover[0] itself at the tail // of ioctl(KCOV_ENABLE). Do not zero header here. // (syzkaller does this unconditionally.) // Make coverage active immediately after enable. let _ = mgr().attach_for_tid(ctx.id, tid); // Emit a small heartbeat so cover > 0 immediately. // Works in either mode (we send both). emit_heartbeats(ctx, mode); Ok(ok0(request)) } KcovIoctl::RemoteEnable => { // Read & validate remote header; we ignore handles for now. let (hdr, _handles_opt) = request.remote_kcov_remote_arg(kcov_arg, false)?; let (mode, req_words) = validate_remote_hdr(&hdr, ctx.words)?; if ctx.words == 0 { // remote_enable without prior init: allow if area_size is sane: mgr().init_trace(ctx.id, req_words as u64)?; ctx.words = req_words; if safe_ftruncate(&ctx.syd_fd, (ctx.words * 8) as i64).is_err() { return Err(Errno::EIO); } // Zero header+payload for a clean view strictly within words. zero_memfd(ctx)?; // fallthrough to enable below } let tid = request.scmpreq.pid().as_raw(); // Enforce single-kcov-per-task (same as KCOV_ENABLE). owner_check(tid, ctx.id)?; // Transition to Enabled (owner recorded in the core). mgr().enable(ctx.id, mode, tid)?; // Publish the owner mapping only after enable; do not clear others. owner_bind(tid, ctx.id); ctx.mode = Some(mode); // Make coverage active immediately after enable. let _ = mgr().attach_for_tid(ctx.id, tid); // Emit a small heartbeat so cover > 0 immediately. // Works in either mode (we send both). emit_heartbeats(ctx, mode); Ok(ok0(request)) } KcovIoctl::Disable => { // Only the owning TID may disable. let tid = request.scmpreq.pid().as_raw(); let ook = { let map = owner_reg().read().unwrap_or_else(|e| e.into_inner()); matches!(map.get(&tid), Some(id) if *id == ctx.id) }; if !ook { return Err(Errno::EINVAL); } // Disable after validation. handle_disable(ctx, tid)?; Ok(ok0(request)) } } } #[inline] fn ok0(req: &UNotifyEventRequest) -> ScmpNotifResp { ScmpNotifResp::new(req.scmpreq.id, 0, 0, 0) } #[inline] fn ensure_len(vec: &mut Vec, need: usize) { if vec.len() < need { vec.resize(need, 0); } } /// Validate a remote KCOV header and compute the effective parameters. /// /// Returns the selected trace mode and the requested area size in words. /// Rejects unknown modes, zero area sizes, and attempts to grow beyond an /// already initialized area. #[inline] fn validate_remote_hdr( hdr: &KCovRemoteHdr, current_words: usize, ) -> Result<(TraceMode, usize), Errno> { let mode = match hdr.trace_mode { 0 => TraceMode::Pc, 1 => TraceMode::Cmp, _ => return Err(Errno::EINVAL), }; if hdr.area_size == 0 { return Err(Errno::EINVAL); } // SAFETY: `area_size` is a u32 from the uapi structure; cast is lossy only // if usize is narrower, which does not happen on supported platforms. #[allow(clippy::cast_possible_truncation)] let req_words = hdr.area_size as usize; if current_words != 0 && req_words > current_words { return Err(Errno::EINVAL); } Ok((mode, req_words)) } /// Attach KCOV for a tracee on syscall dispatch. #[inline] pub fn kcov_enter_for(tid: Pid) -> Result { let tid = tid.as_raw(); // Ask the manager to attach to any instance that has this tid enabled. let id = if let Ok(id) = mgr().attach_for_tid_any(tid) { id } else { // No active coverage for this tid (normal for most syscalls). return Err(Errno::ENOENT); }; // If this is the first attach after RESET (cover[0]==0), // emit a heartbeat so the probe sees non-zero coverage. // Do this *before* the syscall body runs. { let map = reg().read().unwrap_or_else(|e| e.into_inner()); if let Some(ctx) = map.get(&id) { if let Some(mode) = ctx.mode { if let Ok(hdr) = read_header_ne(ctx) { if hdr == 0 { emit_heartbeats(ctx, mode); notice!("ctx":"kcov","op":"attach_heartbeat", "id": id.0, "tid": tid, "msg": format!("emitted post-reset heartbeat for KCOV:{} on attach for tid:{tid}", id.0)); } } } } } notice!("ctx" : "kcov", "op": "attach", "id" : id.0, "tid": tid, "msg": format!("attached to KCOV:{} for tid:{tid}", id.0)); // Scope arms and restores TLS. Ok(KcovScope::new(id)) } /// Internal helper: KCOV_DISABLE semantics for a given context. /// /// Detaches TLS and disables the underlying state; intentionally does /// **not** publish/snapshot into the memfd. The live writer already /// keeps the memfd current during recording. #[inline] fn handle_disable(ctx: &mut KcovCtx, owner_tid: pid_t) -> Result<(), Errno> { // No TLS changes here; TLS is managed by the RAII scope on the worker. // mgr().detach_tls(); // Per-task disable: Drop only this tid from the enable set. mgr().disable_for_tid(ctx.id, owner_tid)?; owner_clear(owner_tid); Ok(()) } /// Internal helper: Emit heartbeats, both PC and CMP. fn emit_heartbeats(ctx: &KcovCtx, mode: TraceMode) { // KCOV heartbeat: write one record immediately so cover > 0 // Write directly to the memfd; // do NOT rely on TLS or background attaches. match mode { TraceMode::Pc => { if ctx.words > 1 { // payload[0] = marker; header = 1 let _ = write_payload_word(ctx, 0, 0xDEADC0DE_u64); let _ = write_header_ne(ctx, 1); } } TraceMode::Cmp => { if ctx.words > 4 { // 1 CMP record (ty, a, b, ip), header = 1 let ty = (8u64 << 8) | 1; // size=8, is_const=1 let _ = write_payload_word(ctx, 0, ty); let _ = write_payload_word(ctx, 1, 0); let _ = write_payload_word(ctx, 2, 0); let _ = write_payload_word(ctx, 3, 0xDEADC0DE_u64); let _ = write_header_ne(ctx, 1); } } } } /// KCOV ioctl(2) requests. #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum KcovIoctl { InitTrace, ResetTrace, Enable, RemoteEnable, UniqueEnable, Disable, } impl TryFrom<(Ioctl, ScmpArch)> for KcovIoctl { type Error = Errno; /// Convert the given ioctl(2) and arch into a `KcovIoctl`. fn try_from(value: (Ioctl, ScmpArch)) -> Result { let names = ioctl_names_get(value.0, value.1).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(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_string()) } } // public: called from main thread on PTRACE_EVENT_EXEC #[inline] pub(crate) fn kcov_exec(exec_tid: Pid, old_tid: Pid) { // When exec changes TGID/TID ownership, // rebind both maps and the core. if old_tid == exec_tid { return; } let old = old_tid.as_raw(); let new = exec_tid.as_raw(); // If this TID owns a KCOV instance, rebind its TGID/owner to new TGID. // Ownership is per-TID (the thread that did KCOV_ENABLE). let id = { let owners = owner_reg().read().unwrap_or_else(|e| e.into_inner()); owners.get(&old).copied() }; if let Some(id) = id { // Also refresh the *owner TID* mapping to the exec thread. owner_rebind_tid(old, new, id); if let Err(errno) = mgr().exec_rebind(id, old, new) { notice!("ctx" : "kcov", "op" : "exec_rebind_mgr", "id": id.0, "tid_old": old, "tid_new": new, "err": errno as i32, "msg": format!("manager denied KCOV:{} exec rebind for TGID:{new} from TID:{old}: {errno}", id.0)); } notice!("ctx":"kcov","op":"exec_rebind", "id": id.0, "tid_old": old, "tid_new": new, "msg": format!("KCOV:{} rebound to exec TGID={new} from TID:{old}", id.0)); } } // public: called from main thread on PTRACE_EVENT_EXIT pub(crate) fn kcov_exit(tid: Pid) { let tid = tid.as_raw(); // If this TID owns a KCOV, auto-disable it. if let Some(id) = { let mut map = owner_reg().write().unwrap_or_else(|e| e.into_inner()); map.remove(&tid) } { // Disable the instance when the enabling task exits and // relax ownership so new subprocess can attach immediately. let _ = mgr().on_owner_exit(id, tid); notice!("ctx" : "kcov", "op" : "owner_exit", "id": id.0, "tid": tid, "msg": format!("disabled KCOV:{} on owner:{tid} exit", id.0)); } // Drop any TLS arm on this worker. mgr().detach_tls(); // NOTE: Do NOT drop/close/disable KCOV contexts on opener/owner exit. } // Recording entry points used by the instrumentation glue. pub(crate) fn record_pc(pc: u64) -> Result<(), Errno> { // Best-effort live memfd update using the TLS sink (tracee_tid). // If no sink is installed on this worker, do nothing. TLS_SINK.with(|sink| { if let Some(s) = *sink.lock().unwrap_or_else(|e| e.into_inner()) { let mut map = reg().write().unwrap_or_else(|e| e.into_inner()); if let Some(ctx) = map.get_mut(&s.id) { live_update_pc_clamped(ctx, pc); } } }); Ok(()) } pub(crate) fn record_cmp( sz: u8, is_const: bool, a: u64, b: u64, ip: u64, ) -> Result<(), nix::errno::Errno> { // Best-effort live memfd update using the TLS sink (tracee_tid). TLS_SINK.with(|sink| { if let Some(s) = *sink.lock().unwrap_or_else(|e| e.into_inner()) { let mut map = reg().write().unwrap_or_else(|e| e.into_inner()); if let Some(ctx) = map.get_mut(&s.id) { live_update_cmp_clamped(ctx, sz, is_const, a, b, ip); } } }); Ok(()) } // Syd: rock-solid application kernel // src/kcov/api.rs — KCOV API utilities // // Copyright (c) 2025 Ali Polatel // SPDX-License-Identifier: GPL-3.0 #![forbid(unsafe_code)] use nix::errno::Errno; /// const FNV-1a 64-bit; fast, deterministic site IDs. pub const fn kcov_hash64(s: &str) -> u64 { let bytes = s.as_bytes(); let mut h: u64 = 0xcbf29ce484222325; let mut i: usize = 0; while i < bytes.len() { h ^= bytes[i] as u64; h = h.wrapping_mul(0x100000001b3); i += 1; } h } /// record a PC edge; no-ops if not enabled (kcov handles TLS/noop) #[inline(always)] pub fn record_pc(pc: u64) -> Result<(), Errno> { // route to the single kcov manager owned by glue crate::kcov::abi::record_pc(pc) } /// record a CMP; sz ∈ {1,2,4,8}; ip is a site id; no-ops if not enabled #[inline(always)] pub fn record_cmp(sz: u8, is_const: bool, a: u64, b: u64, ip: u64) -> Result<(), Errno> { crate::kcov::abi::record_cmp(sz, is_const, a, b, ip) } // --- API macros for coverage; gated by `kcov` feature and no-op when disabled --- /// Emit a lightweight edge at the current callsite using a stable compile-time site ID (no-op when `kcov` is disabled). #[macro_export] macro_rules! kcov_edge { // auto-site: use file:line:col () => {{ const __KCOV_SITE: u64 = $crate::kcov::api::kcov_hash64(concat!(file!(), ":", line!())); let _ = $crate::kcov::api::record_pc(__KCOV_SITE); }}; // user-specified site (any expression → u64) ($site:expr) => {{ let _ = $crate::kcov::api::record_pc(($site) as u64); }}; } /// Emit an edge tagged by a human-readable string hashed at compile time (no-op when `kcov` is disabled). #[macro_export] macro_rules! kcov_edge_site { // compile-time string -> hashed site ($s:literal) => {{ const __KCOV_SITE: u64 = $crate::kcov::api::kcov_hash64($s); let _ = $crate::kcov::api::record_pc(__KCOV_SITE); }}; } /// Record a comparison with automatic site ID so fuzzing can steer value relations (no-op when `kcov` is disabled). #[macro_export] macro_rules! kcov_cmp { // most convenient form: infer ip from callsite ($sz:expr, $isconst:expr, $a:expr, $b:expr) => {{ const __KCOV_SITE: u64 = $crate::kcov::api::kcov_hash64(concat!(file!(), ":", line!())); let _ = $crate::kcov::api::record_cmp( ($sz) as u8, ($isconst), ($a) as u64, ($b) as u64, __KCOV_SITE, ); }}; // explicit site id (u64 or anything → u64) ($sz:expr, $isconst:expr, $a:expr, $b:expr, $site:expr) => {{ let _ = $crate::kcov::api::record_cmp( ($sz) as u8, ($isconst), ($a) as u64, ($b) as u64, ($site) as u64, ); }}; } /// Record a comparison tagged by a human-readable string hashed at compile time (no-op when `kcov` is disabled). #[macro_export] macro_rules! kcov_cmp_site { // compile-time string site ($sz:expr, $isconst:expr, $a:expr, $b:expr, $s:literal) => {{ const __KCOV_SITE: u64 = $crate::kcov::api::kcov_hash64($s); let _ = $crate::kcov::api::record_cmp( ($sz) as u8, ($isconst), ($a) as u64, ($b) as u64, __KCOV_SITE, ); }}; } // Syd: rock-solid application kernel // src/kcov/mod.rs — KCOV userspace ABI shim for syzkaller // // Copyright (c) 2025 Ali Polatel // SPDX-License-Identifier: GPL-3.0 use std::{ fmt, sync::{ atomic::{AtomicU64, Ordering}, Arc, Mutex, RwLock, }, }; use libc::pid_t; use nix::errno::Errno; use serde::{Serialize, Serializer}; use crate::{ hash::{SydHashMap, SydHashSet}, notice, }; // KCOV ABI handlers pub(crate) mod abi; // KCOV API utilities pub(crate) mod api; /// Thread-local sink describing where the live writer should send records. /// /// We intentionally keep this tiny and policy-free: it captures which KCOV /// instance is "armed" on this writer thread and which *tracee* tid owns it. #[derive(Clone, Copy, Debug)] pub(crate) struct TlsSink { pub(crate) id: KcovId, } thread_local! { static TLS_SINK: Mutex> = const { Mutex::new(None) }; } /* #[inline] fn set_tls_sink(id: KcovId) { TLS_SINK.with(|s| *s.lock().unwrap_or_else(|e| e.into_inner()) = Some(TlsSink { id })); } */ #[inline] fn clear_tls_sink() { TLS_SINK.with(|s| *s.lock().unwrap_or_else(|e| e.into_inner()) = None); } // // Public surface // /// KCOV modes (pc/cmp). #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum TraceMode { Pc, Cmp, } // Implement Display manually impl fmt::Display for TraceMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Pc => write!(f, "pc"), Self::Cmp => write!(f, "cmp"), } } } impl Serialize for TraceMode { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Self::Pc => serializer.serialize_str("pc"), Self::Cmp => serializer.serialize_str("cmp"), } } } /// /sys/kernel/debug/kcov handle. #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub struct KcovId(u64); impl KcovId { /// Create a new KcovId. pub const fn new(id: u64) -> Self { Self(id) } } /// Device manager (open/ioctl/mmap lifecycle). pub struct Kcov { seq: AtomicU64, // KcovId -> State map: RwLock>>, } impl Kcov { /// Detach TLS for the current syd worker thread. pub fn detach_tls(&self) { TLS_SINK.with(|tls| { *tls.lock().unwrap_or_else(|e| e.into_inner()) = None; }); } /// new(). pub fn new() -> Self { Self { seq: AtomicU64::new(1), map: RwLock::new(SydHashMap::default()), } } /// open(): create instance. pub fn open(&self) -> Result { let kcov_id = KcovId(self.seq.fetch_add(1, Ordering::Relaxed)); let state_arc = Arc::new(State::new()); let mut write_guard = self.map.write().unwrap_or_else(|e| e.into_inner()); write_guard.insert(kcov_id, state_arc); Ok(kcov_id) } /// KCOV_INIT_TRACE(words). pub fn init_trace(&self, kcov_id: KcovId, words: u64) -> Result<(), Errno> { self.get(kcov_id)?.init_trace(words) } /// KCOV_ENABLE: First enable actives this id for syd. /// Subsequent enables with the same id increases refcount. /// Enabling when other id is active returns EBUSY (single KCOV). pub fn enable(&self, id: KcovId, mode: TraceMode, owner_tid: pid_t) -> Result<(), Errno> { let st = self.get(id)?; st.enable(mode, owner_tid)?; Ok(()) } /// KCOV_DISABLE for the calling tid: drop only that tid from the enable set. /// When the last tid disables, the instance transitions to Disabled. pub fn disable_for_tid(&self, id: KcovId, owner_tid: pid_t) -> Result<(), Errno> { let st = self.get(id)?; st.disable_for_tid(owner_tid)?; // Clear TLS for this worker thread (best-effort). clear_tls_sink(); notice!("ctx" : "kcov", "op" : "disable_tid", "id" : id.0, "tid": owner_tid, "msg": format!("disabled KCOV:{} for tid:{owner_tid}", id.0)); Ok(()) } /// Attach TLS to the given KCOV instance only if `owner_tid` matches. /// /// This avoids the need for a global "active" slot and mirrors the /// per-task semantics of the Linux kernel. pub fn attach_for_tid(&self, id: KcovId, owner_tid: pid_t) -> Result<(), Errno> { let st = self.get(id)?; // Maintain lock ordering TLS -> core to avoid deadlock. let mut res = Ok(()); TLS_SINK.with(|tls| { let mut guard = tls.lock().unwrap_or_else(|e| e.into_inner()); // Establish global lock order: TLS -> core (via pre_attach). // Do the precondition check while holding TLS to match record paths. // Must be Enabled and owned by this tid. res = st.pre_attach_for(owner_tid); if res.is_ok() { *guard = Some(TlsSink { id }); } }); res } /// Attach TLS to whichever KCOV instance currently has `owner_tid` enabled. /// /// This scans all instances and selects the first that accepts `owner_tid` /// via `pre_attach_for()`. It establishes TLS before checking the core to /// maintain the global lock order (TLS -> core). pub fn attach_for_tid_any(&self, owner_tid: pid_t) -> Result { // Snapshot the instances so we don't hold the map lock while probing cores. let instances: Vec<(KcovId, Arc)> = { let rg = self.map.read().unwrap_or_else(|e| e.into_inner()); rg.iter().map(|(k, v)| (*k, v.clone())).collect() }; let mut selected: Option = None; TLS_SINK.with(|tls| { let mut guard = tls.lock().unwrap_or_else(|e| e.into_inner()); for (id, st) in &instances { if st.pre_attach_for(owner_tid).is_ok() { *guard = Some(TlsSink { id: *id }); selected = Some(*id); break; } } }); match selected { Some(id) => Ok(id), None => Err(Errno::ENOENT), } } /// Update the logical owner TGID after exec() (PTRACE_EVENT_EXEC). /// Keeps phase/mode as-is. pub fn exec_rebind( &self, id: KcovId, old_owner_tid: pid_t, new_owner_tid: pid_t, ) -> Result<(), Errno> { let st = self.get(id)?; let mut core = st.core.lock().unwrap_or_else(|e| e.into_inner()); if core.phase != Phase::Enabled { return Err(Errno::EINVAL); } // If old tid had enabled coverage, // move that enable to the new tid. if core.enabled_tids.remove(&old_owner_tid) { core.enabled_tids.insert(new_owner_tid); } Ok(()) } /// The enabling task exited. Keep instance Enabled but clear ownership. pub fn on_owner_exit(&self, id: KcovId, exiting_tid: pid_t) -> Result<(), Errno> { let st = self.get(id)?; let mut core = st.core.lock().unwrap_or_else(|e| e.into_inner()); if core.phase != Phase::Enabled { // Already disabled or not yet enabled. return Ok(()); } if !core.enabled_tids.contains(&exiting_tid) { // Exiting tid had not enabled coverage. return Ok(()); } // Drop the exiting tid from the enable set. core.enabled_tids.remove(&exiting_tid); core.enabled_refcnt = core.enabled_refcnt.saturating_sub(1); let remaining = core.enabled_refcnt; if remaining == 0 { // No more enabled tids: // transition to Disabled (semantics unchanged here). core.mode = None; core.phase = Phase::Disabled; } notice!("ctx" : "kcov", "op" : "disable_on_owner_exit", "id": id.0, "tid": exiting_tid, "msg": format!("dropped exiting tid:{exiting_tid} from KCOV:{} enable set with {remaining} remaining", id.0)); Ok(()) } // -- helpers fn get(&self, kcov_id: KcovId) -> Result, Errno> { let read_guard = self.map.read().unwrap_or_else(|e| e.into_inner()); read_guard.get(&kcov_id).cloned().ok_or(Errno::EBADF) } } // // Internals // #[derive(Copy, Clone, Eq, PartialEq, Debug)] enum Phase { Opened, Init, Enabled, Disabled, Closed, } struct State { core: Mutex, } struct Core { max_words: usize, mode: Option, phase: Phase, enabled_tids: SydHashSet, enabled_refcnt: usize, } impl State { fn new() -> Self { Self { core: Mutex::new(Core { max_words: 0, mode: None, phase: Phase::Opened, enabled_tids: SydHashSet::default(), enabled_refcnt: 0, }), } } // lifecycle fn init_trace(&self, words: u64) -> Result<(), Errno> { if words < 2 || words > (i32::MAX as u64) / 8 { return Err(Errno::EINVAL); } let mut core = self.core.lock().unwrap_or_else(|e| e.into_inner()); if core.phase == Phase::Enabled { // Not allowed while enabled. return Err(Errno::EBUSY); } if core.phase == Phase::Closed { // Treat ops on a closed instance as ENOTTY. return Err(Errno::ENOTTY); } core.max_words = words as usize; core.mode = None; core.phase = Phase::Init; core.enabled_tids.clear(); core.enabled_refcnt = 0; Ok(()) } fn enable(&self, mode: TraceMode, owner_tid: pid_t) -> Result<(), Errno> { let mut core = self.core.lock().unwrap_or_else(|e| e.into_inner()); if core.max_words == 0 { return Err(Errno::EINVAL); } if core.enabled_tids.contains(&owner_tid) { // Duplicate enable by the same tid is not allowed. return Err(Errno::EBUSY); } if core.enabled_refcnt > 0 { // If already enabled, mode must match the first one. if let Some(cur) = core.mode { if cur != mode { return Err(Errno::EINVAL); } } } // reset per-enable (first enable only) if core.enabled_refcnt == 0 { core.mode = Some(mode); core.phase = Phase::Enabled; } // add this tid core.enabled_tids.insert(owner_tid); core.enabled_refcnt = core.enabled_refcnt.saturating_add(1); notice!("ctx" : "kcov", "op" : "enable_core", "owner_tid" : owner_tid, "mode" : mode, "msg" : format!("KCOV state enabled with mode:{mode:?} for tid:{owner_tid} and refcnt:{}", core.enabled_refcnt)); Ok(()) } fn disable_for_tid(&self, owner_tid: pid_t) -> Result<(), Errno> { let mut core = self.core.lock().unwrap_or_else(|e| e.into_inner()); if core.phase != Phase::Enabled { return Err(Errno::EINVAL); } if !core.enabled_tids.remove(&owner_tid) { return Err(Errno::EINVAL); } core.enabled_refcnt = core.enabled_refcnt.saturating_sub(1); /* let remaining = core.enabled_refcnt; if remaining == 0 { notice!("ctx" : "kcov", "op" : "disable_core_last", "msg" : "last tid disabled; transitioning instance to Disabled"); core.mode = None; core.phase = Phase::Disabled; } */ Ok(()) } // Called before TLS attach to ensure the state is usable and owned by `tid`. fn pre_attach_for(&self, tid: pid_t) -> Result<(), Errno> { let core = self.core.lock().unwrap_or_else(|e| e.into_inner()); if core.phase != Phase::Enabled { return Err(Errno::EINVAL); } // Allow only tids that previously enabled this instance. if core.enabled_tids.contains(&tid) { return Ok(()); } notice!("ctx": "kcov", "op": "pre_attach_mismatch", "tid": tid, "mode": core.mode, "msg" : format!("prevented KCOV attach from tid:{tid}; tid is not enabled")); Err(Errno::EPERM) } }