Skip site navigation (1)Skip section navigation (2)
Date:      Mon, 20 Jul 2026 10:25:16 +0000
From:      Alexander Leidinger <netchild@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Subject:   git: 86fa065f1862 - main - hwpmc: drain a process-mode PMC's runcount when a live target detaches
Message-ID:  <6a5df78c.33323.733bf27@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by netchild:

URL: https://cgit.FreeBSD.org/src/commit/?id=86fa065f1862f3b638efa1868523878d9db14ada

commit 86fa065f1862f3b638efa1868523878d9db14ada
Author:     Alexander Leidinger <netchild@FreeBSD.org>
AuthorDate: 2026-07-19 11:39:27 +0000
Commit:     Alexander Leidinger <netchild@FreeBSD.org>
CommitDate: 2026-07-20 10:24:46 +0000

    hwpmc: drain a process-mode PMC's runcount when a live target detaches
    
    A process-mode PMC's runcount tracks how many CPUs currently have it
    loaded in hardware.  It is decremented only by the context-switch-out
    and process-exit reclaim paths, both of which the scheduler invokes
    only for processes flagged P_HWPMC.  Detaching a target that still has
    the PMC live in hardware dropped the target and cleared P_HWPMC without
    taking the PMC off the hardware or dropping the runcount reference, so
    the reference leaked.  A subsequent release then spun in
    pmc_wait_for_pmc_idle() forever waiting for the runcount to reach zero:
    on an INVARIANTS kernel this panics ("waiting too long for pmc to be
    free"), otherwise it is an unkillable loop holding the hwpmc lock.  Any
    process able to allocate a PMC can trigger this by attaching a counting
    PMC to itself and detaching it before releasing.
    
    Take the PMC off the hardware and drop the runcount reference as part
    of detaching, before P_HWPMC is cleared: reclaim it from the detaching
    thread's own CPU directly, and, when the detach removes the PMC's last
    target, wait for any references held by the target's other threads to
    drain while P_HWPMC is still set (they can no longer reload it).
    
    Reviewed by:            adrian
    MFC after:              2 weeks
    Assisted-by:            Claude Code (Fable 5)
    Differential Revision:  https://reviews.freebsd.org/D58342
---
 sys/dev/hwpmc/hwpmc_mod.c | 88 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 88 insertions(+)

diff --git a/sys/dev/hwpmc/hwpmc_mod.c b/sys/dev/hwpmc/hwpmc_mod.c
index 086a05657cfe..efc2aefec2b9 100644
--- a/sys/dev/hwpmc/hwpmc_mod.c
+++ b/sys/dev/hwpmc/hwpmc_mod.c
@@ -225,6 +225,8 @@ static int	pmc_detach_one_process(struct proc *p, struct pmc *pm,
 static void	pmc_destroy_owner_descriptor(struct pmc_owner *po);
 static void	pmc_destroy_pmc_descriptor(struct pmc *pm);
 static void	pmc_destroy_process_descriptor(struct pmc_process *pp);
+static void	pmc_reclaim_pmc_from_cpu(struct pmc *pm,
+    struct pmc_process *pp, int cpu);
 static struct pmc_owner *pmc_find_owner_descriptor(struct proc *p);
 static int	pmc_find_pmc(pmc_id_t pmcid, struct pmc **pm);
 static struct pmc *pmc_find_pmc_descriptor_in_process(struct pmc_owner *po,
@@ -1241,6 +1243,21 @@ pmc_detach_one_process(struct proc *p, struct pmc *pm, int flags)
 	if (pp->pp_pmcs[ri].pp_pmc != pm)
 		return (EINVAL);
 
+	/*
+	 * If this is a process-virtual PMC that is still loaded on the
+	 * hardware of the CPU we are running on (the common case when a
+	 * process detaches a PMC from itself), take it off and drop its
+	 * runcount reference now.  The reference is otherwise only
+	 * dropped by the switch-out reclaim, which the scheduler stops
+	 * calling once P_HWPMC is cleared below - leaking it and later
+	 * wedging pmc_wait_for_pmc_idle() at release time.
+	 */
+	if (PMC_IS_VIRTUAL_MODE(PMC_TO_MODE(pm))) {
+		critical_enter();
+		pmc_reclaim_pmc_from_cpu(pm, pp, curthread->td_oncpu);
+		critical_exit();
+	}
+
 	pmc_unlink_target_process(pm, pp);
 
 	/* Issue a detach entry if a log file is configured */
@@ -1259,6 +1276,22 @@ pmc_detach_one_process(struct proc *p, struct pmc *pm, int flags)
 	if (pp->pp_refcnt != 0)	/* still a target of some PMC */
 		return (0);
 
+	/*
+	 * This detach removed the process' last PMC and we are about to
+	 * clear P_HWPMC.  If the detached PMC was its last target and is
+	 * still loaded on other CPUs (e.g. sibling threads of a
+	 * multi-threaded target, or a target running on another CPU),
+	 * drain those references first: the target is already unlinked so
+	 * it cannot reload the PMC, and P_HWPMC is still set so those
+	 * CPUs' switch-out reclaim still runs.  Bounded by the target
+	 * threads being scheduled out.
+	 */
+	if (PMC_IS_VIRTUAL_MODE(PMC_TO_MODE(pm)) &&
+	    LIST_EMPTY(&pm->pm_targets)) {
+		while (counter_u64_fetch(pm->pm_runcount) > 0)
+			pmc_force_context_switch();
+	}
+
 	pmc_remove_process_descriptor(pp);
 
 	if (flags & PMC_FLAG_REMOVE)
@@ -1612,6 +1645,61 @@ pmc_delta(const struct pmc_classdep *pcd, pmc_value_t newvalue,
 	return (delta);
 }
 
+/*
+ * Take a process-virtual PMC off the hardware of 'cpu' if it is
+ * currently loaded there for process 'pp', accumulating its final
+ * count and dropping its runcount reference.  This is the same reclaim
+ * that context switch out and process exit perform, factored out so it
+ * can also run when a target is detached while the PMC may still be
+ * live: the runcount reference is decremented by the switch-out reclaim
+ * only, which the scheduler gates on P_HWPMC, so a detach that clears
+ * P_HWPMC without draining would leak the reference and later wedge
+ * pmc_wait_for_pmc_idle().  Must be called in a critical section.
+ */
+static void
+pmc_reclaim_pmc_from_cpu(struct pmc *pm, struct pmc_process *pp, int cpu)
+{
+	struct pmc_classdep *pcd;
+	struct pmc *phw_pm;
+	pmc_value_t newvalue, tmp;
+	u_int adjri, ri;
+
+	ri = PMC_TO_ROWINDEX(pm);
+	pcd = pmc_ri_to_classdep(md, ri, &adjri);
+
+	/* Only reclaim if this PMC is actually loaded on this CPU. */
+	phw_pm = NULL;
+	(void)(*pcd->pcd_get_config)(cpu, adjri, &phw_pm);
+	if (phw_pm != pm)
+		return;
+
+	KASSERT(counter_u64_fetch(pm->pm_runcount) > 0,
+	    ("[pmc,%d] pm=%p runcount %ju", __LINE__, pm,
+	    (uintmax_t)counter_u64_fetch(pm->pm_runcount)));
+
+	if (pm->pm_pcpu_state[cpu].pps_cpustate) {
+		pm->pm_pcpu_state[cpu].pps_cpustate = 0;
+		if (pm->pm_pcpu_state[cpu].pps_stalled == 0) {
+			(void)pcd->pcd_stop_pmc(cpu, adjri, pm);
+
+			if (PMC_TO_MODE(pm) == PMC_MODE_TC) {
+				(void)pcd->pcd_read_pmc(cpu, adjri, pm,
+				    &newvalue);
+				tmp = pmc_delta(pcd, newvalue,
+				    PMC_PCPU_SAVED(cpu, ri));
+
+				mtx_pool_lock_spin(pmc_mtxpool, pm);
+				pm->pm_gv.pm_savedvalue += tmp;
+				pp->pp_pmcs[ri].pp_pmcval += tmp;
+				mtx_pool_unlock_spin(pmc_mtxpool, pm);
+			}
+		}
+	}
+
+	counter_u64_add(pm->pm_runcount, -1);
+	(void)pcd->pcd_config_pmc(cpu, adjri, NULL);
+}
+
 /*
  * Thread context switch OUT.
  */


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a5df78c.33323.733bf27>