| // Syd: rock-solid application kernel
|
| // src/kcov/abi.rs — KCOV ABI handlers
|
| //
|
| // Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
|
| // 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<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))
|
| }
|
|
|
| /// 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<TraceMode>,
|
| pub(crate) scratch: Vec<u8>,
|
| }
|
|
|
| //
|
| // Singletons
|
| //
|
|
|
| static KCOV_REG: OnceLock<RwLock<SydHashMap<KcovId, KcovCtx>>> = OnceLock::new();
|
| fn reg() -> &'static RwLock<SydHashMap<KcovId, KcovCtx>> {
|
| KCOV_REG.get_or_init(|| RwLock::new(SydHashMap::default()))
|
| }
|
|
|
| /// Map owner (enabling) TID -> KcovId.
|
| static KCOV_OWNER_REG: OnceLock<RwLock<SydHashMap<pid_t, KcovId>>> = OnceLock::new();
|
| fn owner_reg() -> &'static RwLock<SydHashMap<pid_t, KcovId>> {
|
| 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<Kcov> = 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<TlsSink>,
|
| _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<MaybeFd, Errno> {
|
| 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<ScmpNotifResp, Errno> {
|
| 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<u8>, 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<KcovScope, Errno> {
|
| 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<Self, Errno> {
|
| 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
| 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 <alip@chesswob.org>
|
| // 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 <alip@chesswob.org>
|
| // 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<Option<TlsSink>> = 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
| 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<SydHashMap<KcovId, Arc<State>>>,
|
| }
|
|
|
| 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<KcovId, Errno> {
|
| 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<KcovId, Errno> {
|
| // Snapshot the instances so we don't hold the map lock while probing cores.
|
| let instances: Vec<(KcovId, Arc<State>)> = {
|
| 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<KcovId> = 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<Arc<State>, 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<Core>,
|
| }
|
|
|
| struct Core {
|
| max_words: usize,
|
| mode: Option<TraceMode>,
|
| phase: Phase,
|
| enabled_tids: SydHashSet<pid_t>,
|
| 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)
|
| }
|
| }
|