//
// Syd: rock-solid application kernel
// src/kcov/abi.rs: KCOV ABI handlers
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
// SPDX-License-Identifier: GPL-3.0

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,
};

// Set the per-thread syscall number in the C stub's TLS.
extern "C" {
    fn syd_kcov_set_syscall(nr: c_long);
}

// Per-thread syscall number (Rust mirror of the C stub's tls_sys).
//
// Syd processes all syscalls through the same internal code paths,
// producing identical PCs regardless of the syscall type. Mixing the
// active syscall number into each PC makes the same Syd function
// produce different signal for different syscalls.
//
// Used by emit_heartbeats to mix the syscall number into heartbeat PCs,
// keeping them consistent with the instrumented PCs.
thread_local! {
    static TLS_SYS: Cell<c_long> = const { Cell::new(-1) };
}

// Per-thread cached KCOV context for lock-free access from
// sanitizer hooks. Populated by kcov_enter_for() while holding
// the read lock, consumed by syd_kcov_get_ctx() without any lock.
//
// Fields: (fd, words, mode) where mode: 0=PC, 1=CMP, -1=None.
thread_local! {
    static TLS_CTX: Cell<(i32, u64, i32)> = const { Cell::new((-1, 0, -1)) };
}

// Thread-local flag to prevent recursion in coverage hooks.
thread_local! {
    static TLS_REC: Cell<bool> = const { Cell::new(false) };
}

// RAII guard to temporarily disable KCOV coverage collection on the current thread.
// Used to prevent deadlocks when acquiring the `kcov_reg()` write lock.
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));
    }
}

// Set the active syscall number for KCOV coverage diversity.
//
// Must be called before `kcov_enter_for` so that heartbeat PCs and
// sanitizer-hook PCs both carry the same syscall tag.
pub(crate) fn kcov_set_syscall(nr: c_long) {
    // Set Rust-side TLS for heartbeat mixing.
    TLS_SYS.with(|c| c.set(nr));

    // SAFETY: Set C-side TLS for sanitizer hook mixing.
    unsafe { syd_kcov_set_syscall(nr) };
}

// Mix syscall number into a PC.
//
// Must produce the same transform as the C version.
// See: src/kcov/stub.c
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
}

// Map a stable site ID into a PC-shaped value.
//
// Must produce the same transform as the C version.
// See: src/kcov/stub.c
const fn to_canon_pc(v: u64) -> u64 {
    // Enforce 16-byte alignment.
    let v = v & !0xFu64;

    if cfg!(target_pointer_width = "64") {
        // Map into kernel text range that syzkaller expects.
        // Base: 0xFFFFFFFF_80000000 Mask: 0x3FFF_FFF0 (~1 GiB, 16B aligned)
        // No bit overlap between base and mask, so OR == addition.
        0xFFFF_FFFF_8000_0000u64 | (v & 0x3FFF_FFF0u64)
    } else {
        // 32-bit: put into high region, keep alignment.
        let x = (v as u32) & 0x0FFF_FFF0u32;
        (0x8000_0000u32 | x) as u64
    }
}

// Compute payload capacity (in records) for the given context/mode.
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,
    }
}

// Encode KCOV comparison type.
//
// type bit 0   : KCOV_CMP_CONST
// type bits 1-2: size code (1->0, 2->2, 4->4, 8->6)
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)
}

// Read cover[0] (native-endian u64) from the memfd.
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))
}

// Write cover[0] (native-endian u64) to the memfd.
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))
}

// Write a single payload u64 at record index `idx` (0-based).
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();
    ctx.syd_fd
        .write_all_at(&bytes, off)
        .map_err(|err| err2no(&err))
}

// Zero the live memfd header+payload strictly within `words`.
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))?;

    // Flush to page cache to ensure mmap(2) visibility.
    ctx.syd_fd.sync_data().or(Err(Errno::EIO))
}

// Best-effort live memfd update for a PC record with capacity clamp.
//
// If full, clamps header to capacity and performs no payload write.
fn live_update_pc_clamped(ctx: &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;
    }

    // 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 {
        // Clamp header if it drifted past cap; ignore errors.
        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);
}

// 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();
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)
}

//
// Public API
//

// Create a named memfd for kcov, register per-TID context.
#[allow(clippy::cognitive_complexity)]
pub(crate) fn kcov_open(_tid: Pid) -> Result<MaybeFd, Errno> {
    // Create memfd and get its inode.
    let memfd = safe_memfd_create(c"syd-kcov", *SAFE_MFD_FLAGS)?.into_raw_fd();

    // SAFETY: seccomp addfd creates a duplicate.
    let memfd_own = unsafe { OwnedFd::from_raw_fd(memfd) };

    // Register device state in the manager based on inode.
    let kcov_id = fd_inode(&memfd_own)?;
    kcov_mgr().open(kcov_id)?;

    // Register per-TID context (disabled until KCOV_ENABLE).
    {
        let kcov_id = KcovId(kcov_id);
        let _snap = KcovSnap::new(); // pause coverage during kcov_reg().
        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(),
            },
        );
    }

    // Hand the original memfd back to caller.
    // This fd will not be closed on drop.
    Ok(memfd.into())
}

// Argument for KCOV_REMOTE_ENABLE ioctl.
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
struct KcovRemoteArg {
    trace_mode: u32,
    area_size: u32,
    num_handles: u32,
    common_handle: u64,
}

// 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();

    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 memfd inode.
    let kcov_id = match proc_kcov_read_id(tid, fd) {
        Ok(id) => id,
        Err(_) => return Err(Errno::ENOTTY),
    };

    // Decode ioctl by NAME (arch-safe).
    // Linux kernel truncates cmd to unsigned int.
    #[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)?;

            // Acquire the write lock after init trace to avoid deadlock.
            let _snap = KcovSnap::new(); // pause coverage during kcov_reg().
            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),
            };

            // Track/resize our memfd view to match `words`.
            ctx.words = words as usize;
            safe_ftruncate(&ctx.syd_fd, (ctx.words * 8) as i64)?;

            // Seal memfd for seals, shrinks and grows.
            // Writes are permitted.
            let flags = SealFlag::F_SEAL_SEAL | SealFlag::F_SEAL_SHRINK | SealFlag::F_SEAL_GROW;
            seal_memfd(&ctx.syd_fd, flags)?;

            // Zero the file (header+payload) strictly within words.
            zero_memfd(ctx)?;

            Ok(ok0(request))
        }

        KcovIoctl::Enable => {
            // Register TID -> KcovId mapping for this thread.
            set_kcov_tid(tid, kcov_id, false);

            let mode = match kcov_arg {
                0 => TraceMode::Pc,
                1 => TraceMode::Cmp,
                _ => return Err(Errno::EINVAL),
            };

            // Look up per-TID context.
            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
            };

            // Transition manager to Enabled.
            kcov_mgr().enable(id, mode)?;

            // Publish the mode into our local ctx afterwards.
            {
                let _snap = KcovSnap::new(); // pause coverage during kcov_reg().
                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();

            // SAFETY: Reinterpreting a POD struct as a byte slice for reading.
            let buf = unsafe {
                std::slice::from_raw_parts_mut(
                    &raw mut arg as *mut u8,
                    std::mem::size_of::<KcovRemoteArg>(),
                )
            };

            // Read the structure from tracee memory.
            // The request.read_mem() method validates the request.
            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),
            };

            // Look up per-TID context.
            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
            };

            // Register TID -> KcovId mapping for this thread.
            set_kcov_tid(tid, id, true);

            // Transition manager to Enabled.
            kcov_mgr().enable(id, mode)?;

            // Publish the mode into our local ctx afterwards.
            {
                let _snap = KcovSnap::new(); // pause coverage during kcov_reg().
                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 the TID -> KcovId mapping to prevent
            // stale entries from being reused after PID recycling.
            remove_kcov_tid(tid);

            // Look up per-TID context.
            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 => {
            // Zero the header+payload to reset coverage count.
            // Keep the KCOV in Enabled phase, don't detach TLS.
            let _snap = KcovSnap::new(); // pause coverage during kcov_reg().
            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 entire memfd (header + payload).
            zero_memfd(ctx)?;

            // Keep one deterministic heartbeat after reset so userspace
            // readers can observe non-empty coverage even if the following
            // syscall is not on seccomp-notify path.
            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
}

// Attach KCOV for a thread on syscall dispatch.
pub(crate) fn kcov_enter_for(tid: Pid) -> Result<(), Errno> {
    // Use get_kcov_tid(tid) as source of truth.
    let kcov_id = match get_kcov_tid(tid) {
        Some(id) => {
            set_tls_sink(id);
            id
        }
        None => {
            clear_tls_sink();
            return Ok(());
        }
    };

    // Validate context exists and has a mode set.
    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(());
    }

    // Cache context in TLS for lock-free access from sanitizer hooks.
    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)));

    // Emit heartbeat.
    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(())
}

// Flush KCOV coverage at syscall exit.
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(()),
    };

    // Read the current header to see how many records were written.
    let cnt = match read_header_ne(ctx) {
        Ok(n) => n,
        Err(_) => return Ok(()),
    };

    // Flush to page cache so mmap(2) readers see
    // all accumulated data from this syscall's coverage hooks.
    if cnt > 0 {
        let _ = ctx.syd_fd.sync_data();
    }

    Ok(())
}

// Emit a heartbeat for coverage.
fn emit_heartbeats(file: &File, words: usize, mode: TraceMode) {
    match mode {
        TraceMode::Pc => {
            if words > 1 {
                // payload[0] = marker; header = 1
                let pc = to_canon_pc(mix_syscall(KCOV_HEART_BEAT)).to_ne_bytes();
                let hdr = 1u64.to_ne_bytes();

                // Write payload[0] at offset 8.
                if file.write_all_at(&pc, 8).is_err() {
                    return;
                }
                // Write header at offset 0.
                if file.write_all_at(&hdr, 0).is_err() {
                    return;
                }

                // Flush data to page cache to ensure mmap(2) visibility.
                let _ = file.sync_data();
            }
        }
        TraceMode::Cmp => {
            if words > 4 {
                // 1 CMP record (ty, a, b, ip), header = 1
                // Keep is_const=0 and arg1!=0, so syzkaller does not
                // drop this comparison as an uninteresting (0,0,const) cmp.
                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();

                // Write payload at offsets 8, 16, 24, 32.
                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;
                }
                // Write header.
                if file.write_all_at(&hdr, 0).is_err() {
                    return;
                }

                // Flush data to page cache to ensure mmap(2) visibility.
                let _ = file.sync_data();
            }
        }
    }
}

// Attach KCOV for the given PID, setting up TLS sink.
pub(crate) fn kcov_attach(pid: Pid) {
    if let Some(id) = get_kcov_tid(pid) {
        // Set TLS for this worker thread, as different Syd workers may
        // handle different syscalls for the same process.
        set_tls_sink(id);

        // Emit heartbeat (best-effort).
        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);
    }
}

//
// FFI exports for C stub to access KCOV context
//

#[repr(C)]
pub(crate) struct kcov_ctx {
    // Memory file descriptor
    pub(crate) fd: RawFd,
    // Buffer size in words
    pub(crate) words: u64,
    // 0=PC, 1=CMP
    pub(crate) mode: i32,
}

/// Get KCOV context for given ID.
///
/// Called from sanitizer hooks which may fire while kcov_enter_for()
/// holds a read lock. Reads from TLS cache populated by kcov_enter_for().
/// No lock required.
#[no_mangle]
pub extern "C" fn syd_kcov_get_ctx(out_ctx: *mut kcov_ctx) -> bool {
    if out_ctx.is_null() {
        return false;
    }

    // Context is identified by TLS, not by KCOV id.
    let (fd, words, mode) = TLS_CTX.with(|c| c.get());
    if fd < 0 || words == 0 {
        return false;
    }

    // SAFETY: FFI boundary, out_ctx is trusted.
    unsafe {
        (*out_ctx).fd = fd;
        (*out_ctx).words = words;
        (*out_ctx).mode = mode;
    }
    true
}

//
// Sanitizer Coverage Hooks
//

#[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 (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())
    }
}

// Recording entry points used by the instrumentation glue.
#[inline(never)]
pub(crate) fn record_pc(pc: u64) -> Result<(), Errno> {
    // Fast thread-local check to prevent any recursion.
    if TLS_REC.with(|c| {
        if c.get() {
            true
        } else {
            c.set(true);
            false
        }
    }) {
        return Ok(());
    }

    // Convert to kernel-like canonical address.
    let pc = to_canon_pc(pc);

    // Best-effort live memfd update using the TLS sink.
    // If no sink is installed on this worker, do nothing.
    // Block on read lock to ensure coverage is always recorded.
    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(())
}
