Linux Perf Users
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Jinchao Wang" <wangjinchao600@gmail.com>
Cc: linux-perf-users@vger.kernel.org
Subject: Re: [RFC PATCH v2 08/13] mm/kwatch: add hardware breakpoint backend
Date: Fri, 17 Jul 2026 13:24:02 +0000	[thread overview]
Message-ID: <20260717132402.E8BD51F000E9@smtp.kernel.org> (raw)
In-Reply-To: <20260717130520.1902926-1-wangjinchao600@gmail.com>

Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] Modifying the trace_seq buffer directly during TP_printk argument evaluation prepends the stack trace and corrupts the trace output.
- [High] Race condition in CPU offline callback leading to Use-After-Free in the IPI handler.
- [High] Concurrent read and write of the shared `wp->attr` structure across CPUs can result in torn reads and invalid hardware breakpoint configuration.
- [High] Untracked csd_arm IPIs can execute after the watchpoint is freed if the target CPU goes offline.
- [High] The `wp->arm_tsk` pointer is assigned to `current` without holding a reference, causing a Use-After-Free if the task exits.
- [High] Missing NULL check on `register_wide_hw_breakpoint` return value causes a kernel panic if hardware breakpoints are unsupported.
- [Medium] Newly onlined CPUs are unconditionally forced to watch the dummy address, missing hits if the watchpoint is currently active.
- [Medium] Per-CPU throttling of arm IPIs causes unrelated watchpoints to be silently ignored on remote CPUs.
- [Medium] A race gap between `register_wide_hw_breakpoint` and `cpuhp_setup_state_nocalls` drops watchpoints on concurrently hotplugged CPUs.
--

--- Patch [8]: [RFC PATCH v2 08/13] mm/kwatch: add hardware breakpoint backend ---
commit 6c7794f69c285985dbf3f0583ef594237690a973
Author: Jinchao Wang <wangjinchao600@gmail.com>

mm/kwatch: add hardware breakpoint backend

Manage a preallocated pool of wide (per-CPU) perf hardware breakpoints.
All breakpoints are registered up front against a dummy address; arming a
watchpoint only re-points an already-registered event, so the arm path can
run from a kprobe handler.

> diff --git a/include/trace/events/kwatch.h b/include/trace/events/kwatch.h
> index 0000000000000..8a2ec6811ad49
> --- /dev/null
> +++ b/include/trace/events/kwatch.h

[ ... ]

> +	TP_printk("KWatch HIT: time=%llu.%06u ip=%pS addr=0x%lx%s",
> +		  div_u64(__entry->time_ns, 1000000000ULL),
> +		  (unsigned int)(div_u64(__entry->time_ns, 1000ULL) % 1000000ULL),
> +		  (void *)__entry->ip, __entry->addr,
> +		  kwatch_trace_print_stack(p, __get_dynamic_array(stack),
> +					   __entry->stack_nr))

[Severity: High]
Does evaluating kwatch_trace_print_stack() as a TP_printk() argument modify
the trace_seq buffer prematurely?

Because C argument evaluation order is unsequenced, calling trace_seq_printf()
directly inside kwatch_trace_print_stack() mutates the trace_seq buffer p
before or while the main format string is processed.

This causes the output to be written out-of-order or truncated.

> diff --git a/mm/kwatch/hwbp.c b/mm/kwatch/hwbp.c
> index 0000000000000..d1e93754cce82
> --- /dev/null
> +++ b/mm/kwatch/hwbp.c

[ ... ]

> +static void kwatch_hwbp_handler(struct perf_event *bp,
> +				struct perf_sample_data *data,
> +				struct pt_regs *regs)
> +{
> +	struct kwatch_watchpoint *wp = bp->overflow_handler_context;
> +	unsigned long stack_entries[KWATCH_STACK_DEPTH];
> +	unsigned int stack_nr;
> +
> +	if (!kwatch_probe_validate_hit(regs, wp->arm_tsk))
> +		return;

[Severity: High]
Does this code dereference a freed task_struct if the arming task exits?

When armed, wp->arm_tsk is assigned to current without taking a reference
count. If the task exits, a subsequent breakpoint hit will dereference a
freed task, leading to a use-after-free and kernel panic.

[ ... ]

> +static void kwatch_hwbp_arm_local(void *info)
> +{
> +	struct kwatch_watchpoint *wp = info;
> +	struct perf_event *bp;
> +	unsigned long flags;
> +	int cpu, err;
> +
> +	local_irq_save(flags);
> +
> +	cpu = smp_processor_id();
> +	bp = per_cpu(*wp->event, cpu);

[Severity: High]
Can this dereference a freed perf_event or a NULL pointer?

If a concurrent CPU offline event unregisters and frees the perf_event, the
per-CPU pointer might be read here before it is nullified in
kwatch_hwbp_cpu_offline().

Additionally, if register_wide_hw_breakpoint() returned NULL during
initialization (which IS_ERR_PCPU does not catch), this will attempt to
access the NULL pointer.

> +
> +	if (unlikely(!bp))
> +		goto out;
> +
> +	kwatch_probe_mute(true);
> +	barrier();
> +
> +	/*
> +	 * On success this also updates the per-CPU bp->attr, so the hit
> +	 * handler reports what THIS CPU is watching instead of the shared
> +	 * wp->attr, which another CPU may be re-pointing.
> +	 */
> +	err = modify_wide_hw_breakpoint_local(bp, &wp->attr);

[Severity: High]
Can this read a torn or partially updated wp->attr structure?

The shared wp->attr structure is modified by kwatch_hwbp_arm() on another
CPU without locking or WRITE_ONCE(). Remote CPUs executing this IPI
concurrently can observe torn reads, leading to an invalid hardware
breakpoint configuration.

[ ... ]

> +static int kwatch_hwbp_cpu_online(unsigned int cpu)
> +{
> +	struct perf_event_attr attr;
> +	struct kwatch_watchpoint *wp;
> +	struct perf_event *bp;
> +
> +	mutex_lock(&kwatch_all_wp_mutex);
> +	list_for_each_entry(wp, &kwatch_all_wp_list, list) {
> +		attr = wp->attr;
> +		attr.bp_addr = (unsigned long)&kwatch_dummy_holder;

[Severity: Medium]
Will this unconditional override cause a newly onlined CPU to miss active hits?

If a watchpoint is currently armed and active, bringing a CPU online forces
the new CPU to watch the dummy address instead of the currently watched
address.

[ ... ]

> +static int kwatch_hwbp_cpu_offline(unsigned int cpu)
> +{
> +	struct kwatch_watchpoint *wp;
> +	struct perf_event *bp;
> +
> +	mutex_lock(&kwatch_all_wp_mutex);
> +	list_for_each_entry(wp, &kwatch_all_wp_list, list) {
> +		bp = per_cpu(*wp->event, cpu);
> +		if (bp) {
> +			unregister_hw_breakpoint(bp);
> +			per_cpu(*wp->event, cpu) = NULL;

[Severity: High]
Does unregistering the breakpoint before setting the pointer to NULL create a
race condition?

If an IPI broadcast from kwatch_hwbp_arm() occurs concurrently, it can read
the stale pointer in kwatch_hwbp_arm_local() and access the freed memory.

> +		}
> +	}
> +	mutex_unlock(&kwatch_all_wp_mutex);
> +	return 0;
> +}

[ ... ]

> +void kwatch_hwbp_arm(struct kwatch_watchpoint *wp, unsigned long addr, u16 len)
> +{
> +	static DEFINE_PER_CPU(u64, last_ipi_time);
> +	int cur_cpu;
> +	call_single_data_t *csd;
> +	int cpu;
> +	bool is_disarm = (addr == (unsigned long)&kwatch_dummy_holder);
> +	bool skip_remote = false;
> +
> +	wp->attr.bp_addr = addr;
> +	wp->attr.bp_len = len;

[Severity: High]
Can mutating these shared fields concurrently cause torn reads?

Because wp->attr is modified without locking or WRITE_ONCE(), remote CPUs
processing the arm IPI via modify_wide_hw_breakpoint_local() might read
partially updated states.

> +
> +	if (!is_disarm)
> +		wp->arm_tsk = current;

[Severity: High]
Can this result in a use-after-free on current?

No reference count is taken when storing the current task pointer. If the
task exits, a subsequent breakpoint hit will dereference a freed task.

> +
> +	/* ensure attr update visible to other cpu before sending IPI */
> +	smp_wmb();
> +
> +	atomic_set(&wp->pending_ipis, 1);
> +	cur_cpu = get_cpu();
> +
> +	/*
> +	 * Rate-limit only the cross-CPU broadcast, never the local re-point.
> +	 * Arming the current CPU is free and must always reflect this window;
> +	 * only the remote IPI fan-out is throttled to keep a hot function from
> +	 * storming every CPU. A suppressed broadcast means remote CPUs keep
> +	 * watching the previous address for that window (a missed remote-CPU
> +	 * writer is possible) - hence the visible counter, and why kwatch
> +	 * targets low-frequency functions. Disarm is never throttled: the
> +	 * slot must always be released.
> +	 */
> +	if (!is_disarm) {
> +		u64 now = local_clock();
> +		u64 last = this_cpu_read(last_ipi_time);
> +
> +		if (now - last < KWATCH_ARM_IPI_MIN_INTERVAL_NS) {
> +			atomic_long_inc(&kwatch_arm_ipi_suppressed);
> +			skip_remote = true;

[Severity: Medium]
Will this per-CPU throttling drop arm IPIs for entirely independent watchpoints?

Since last_ipi_time is stored per-CPU rather than per-watchpoint, arming two
distinct watchpoints in rapid succession from the same CPU will cause the
second watchpoint to be silently ignored on remote CPUs.

> +		} else {
> +			this_cpu_write(last_ipi_time, now);
> +		}
> +	}
> +
> +	if (!skip_remote) {
> +		for_each_online_cpu(cpu) {
> +			if (cpu == cur_cpu)
> +				continue;
> +
> +			if (is_disarm)
> +				atomic_inc(&wp->pending_ipis);
> +
> +			csd = per_cpu_ptr(is_disarm ? wp->csd_disarm : wp->csd_arm,
> +					  cpu);

[Severity: High]
Can leaving arm IPIs untracked result in a use-after-free?

Because only disarm IPIs are tracked by pending_ipis, an active csd_arm IPI
might still be queued on an offline-going CPU when kwatch_hwbp_free() is
called and frees the csd_arm structure.

[ ... ]

> +void kwatch_hwbp_free(void)
> +{
> +	struct kwatch_watchpoint *wp, *tmp;
> +
> +	kwatch_wp_nr = 0;
> +	kfree(kwatch_wp_slots);
> +	kwatch_wp_slots = NULL;
> +
> +	if (kwatch_hwbp_cpuhp_state != CPUHP_INVALID) {
> +		cpuhp_remove_state_nocalls(kwatch_hwbp_cpuhp_state);
> +		kwatch_hwbp_cpuhp_state = CPUHP_INVALID;
> +	}
> +
> +	mutex_lock(&kwatch_all_wp_mutex);
> +	list_for_each_entry_safe(wp, tmp, &kwatch_all_wp_list, list) {
> +		list_del(&wp->list);
> +
> +		WRITE_ONCE(wp->teardown, true);
> +		atomic_dec(&wp->refcount);
> +
> +		/* Wait for all async IPIs to finish */
> +		while (atomic_read(&wp->refcount) > 0)
> +			cpu_relax();
> +
> +		unregister_wide_hw_breakpoint(wp->event);
> +		free_percpu(wp->csd_arm);

[Severity: High]
Is it safe to free csd_arm here while untracked arm IPIs might still be pending?

Since csd_arm IPIs are not tracked by pending_ipis, a delayed or queued
arm IPI on an offline-going CPU could access the freed csd_arm.

[ ... ]

> +int kwatch_hwbp_prealloc(u16 max_watch)
> +{
> +	struct kwatch_watchpoint *wp;
> +	int success = 0, cpu;
> +	int ret;

[ ... ]

> +		wp->event = register_wide_hw_breakpoint(&wp->attr,
> +							kwatch_hwbp_handler,
> +							wp);
> +		if (IS_ERR_PCPU(wp->event)) {

[Severity: High]
Can wp->event be NULL if hardware breakpoints are unsupported?

register_wide_hw_breakpoint() can return NULL. The IS_ERR_PCPU() check does
not catch a NULL return value, causing a kernel panic when
kwatch_hwbp_arm_local() later attempts to dereference the NULL pointer.

[Severity: Medium]
Does registering breakpoints before setting up CPU hotplug callbacks create a
race gap?

If a CPU comes online between this register_wide_hw_breakpoint() loop and the
cpuhp_setup_state_nocalls() below, the new CPU will miss the watchpoint
creation.

[ ... ]

> +	ret = cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN, "kwatch:online",
> +					kwatch_hwbp_cpu_online,
> +					kwatch_hwbp_cpu_offline);
> +	if (ret < 0) {
> +		kwatch_hwbp_free();
> +		return ret;
> +	}
> +
> +	kwatch_hwbp_cpuhp_state = ret;
> +	return 0;
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260717125023.1895892-1-wangjinchao600@gmail.com?part=8

  reply	other threads:[~2026-07-17 13:24 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-17 12:50 [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Jinchao Wang
2026-07-17 13:02 ` [RFC PATCH v2 01/13] arch: add HAVE_REINSTALL_HW_BREAKPOINT Jinchao Wang
2026-07-17 13:02 ` [RFC PATCH v2 02/13] x86/hw_breakpoint: Unify breakpoint install/uninstall Jinchao Wang
2026-07-17 13:15   ` sashiko-bot
2026-07-17 13:03 ` [RFC PATCH v2 03/13] x86/hw_breakpoint: Add arch_reinstall_hw_breakpoint Jinchao Wang
2026-07-17 13:03 ` [RFC PATCH v2 04/13] HWBP: Add modify_wide_hw_breakpoint_local() API Jinchao Wang
2026-07-17 13:21   ` sashiko-bot
2026-07-17 13:04 ` [RFC PATCH v2 05/13] mm/kwatch: add watch expression parser and dereference engine Jinchao Wang
2026-07-17 13:15   ` sashiko-bot
2026-07-17 13:04 ` [RFC PATCH v2 06/13] mm/kwatch: add lockless per-task context pool Jinchao Wang
2026-07-17 13:19   ` sashiko-bot
2026-07-17 13:04 ` [RFC PATCH v2 07/13] stacktrace: export stack_trace_save_regs() Jinchao Wang
2026-07-17 13:05 ` [RFC PATCH v2 08/13] mm/kwatch: add hardware breakpoint backend Jinchao Wang
2026-07-17 13:24   ` sashiko-bot [this message]
2026-07-17 13:05 ` [RFC PATCH v2 09/13] mm/kwatch: add probe lifecycle runtime Jinchao Wang
2026-07-17 13:20   ` sashiko-bot
2026-07-17 13:06 ` [RFC PATCH v2 10/13] mm/kwatch: add anchor thread for global watchpoints Jinchao Wang
2026-07-17 13:20   ` sashiko-bot
2026-07-17 13:06 ` [RFC PATCH v2 11/13] mm/kwatch: add debugfs control plane Jinchao Wang
2026-07-17 13:27   ` sashiko-bot
2026-07-17 13:07 ` [RFC PATCH v2 12/13] mm/kwatch: add KUnit tests for the watch expression parser Jinchao Wang
2026-07-17 13:07 ` [RFC PATCH v2 13/13] Documentation/dev-tools: document KWatch Jinchao Wang
2026-07-17 13:30   ` sashiko-bot
2026-07-17 13:41 ` [RFC PATCH v2 00/13] mm/kwatch: dynamic hardware watchpoints for hunting memory corruption Dave Hansen

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260717132402.E8BD51F000E9@smtp.kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=linux-perf-users@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=wangjinchao600@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox