Skip to content

Commit

Permalink
powerpc/watchpoint: Don't allow concurrent perf and ptrace events
Browse files Browse the repository at this point in the history
With Book3s DAWR, ptrace and perf watchpoints on powerpc behaves
differently. Ptrace watchpoint works in one-shot mode and generates
signal before executing instruction. It's ptrace user's job to
single-step the instruction and re-enable the watchpoint. OTOH, in
case of perf watchpoint, kernel emulates/single-steps the instruction
and then generates event. If perf and ptrace creates two events with
same or overlapping address ranges, it's ambiguous to decide who
should single-step the instruction. Because of this issue, don't
allow perf and ptrace watchpoint at the same time if their address
range overlaps.

Signed-off-by: Ravi Bangoria <ravi.bangoria@linux.ibm.com>
Signed-off-by: Michael Ellerman <mpe@ellerman.id.au>
Reviewed-by: Michael Neuling <mikey@neuling.org>
Link: https://lore.kernel.org/r/20200514111741.97993-15-ravi.bangoria@linux.ibm.com
  • Loading branch information
Ravi Bangoria authored and Michael Ellerman committed May 18, 2020
1 parent 74c6881 commit 29da4f9
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 0 deletions.
2 changes: 2 additions & 0 deletions arch/powerpc/include/asm/hw_breakpoint.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ extern int hw_breakpoint_exceptions_notify(struct notifier_block *unused,
unsigned long val, void *data);
int arch_install_hw_breakpoint(struct perf_event *bp);
void arch_uninstall_hw_breakpoint(struct perf_event *bp);
int arch_reserve_bp_slot(struct perf_event *bp);
void arch_release_bp_slot(struct perf_event *bp);
void arch_unregister_hw_breakpoint(struct perf_event *bp);
void hw_breakpoint_pmu_read(struct perf_event *bp);
extern void flush_ptrace_hw_breakpoint(struct task_struct *tsk);
Expand Down
221 changes: 221 additions & 0 deletions arch/powerpc/kernel/hw_breakpoint.c
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,227 @@ static bool is_ptrace_bp(struct perf_event *bp)
return bp->overflow_handler == ptrace_triggered;
}

struct breakpoint {
struct list_head list;
struct perf_event *bp;
bool ptrace_bp;
};

static DEFINE_PER_CPU(struct breakpoint *, cpu_bps[HBP_NUM_MAX]);
static LIST_HEAD(task_bps);

static struct breakpoint *alloc_breakpoint(struct perf_event *bp)
{
struct breakpoint *tmp;

tmp = kzalloc(sizeof(*tmp), GFP_KERNEL);
if (!tmp)
return ERR_PTR(-ENOMEM);
tmp->bp = bp;
tmp->ptrace_bp = is_ptrace_bp(bp);
return tmp;
}

static bool bp_addr_range_overlap(struct perf_event *bp1, struct perf_event *bp2)
{
__u64 bp1_saddr, bp1_eaddr, bp2_saddr, bp2_eaddr;

bp1_saddr = ALIGN_DOWN(bp1->attr.bp_addr, HW_BREAKPOINT_SIZE);
bp1_eaddr = ALIGN(bp1->attr.bp_addr + bp1->attr.bp_len, HW_BREAKPOINT_SIZE);
bp2_saddr = ALIGN_DOWN(bp2->attr.bp_addr, HW_BREAKPOINT_SIZE);
bp2_eaddr = ALIGN(bp2->attr.bp_addr + bp2->attr.bp_len, HW_BREAKPOINT_SIZE);

return (bp1_saddr < bp2_eaddr && bp1_eaddr > bp2_saddr);
}

static bool alternate_infra_bp(struct breakpoint *b, struct perf_event *bp)
{
return is_ptrace_bp(bp) ? !b->ptrace_bp : b->ptrace_bp;
}

static bool can_co_exist(struct breakpoint *b, struct perf_event *bp)
{
return !(alternate_infra_bp(b, bp) && bp_addr_range_overlap(b->bp, bp));
}

static int task_bps_add(struct perf_event *bp)
{
struct breakpoint *tmp;

tmp = alloc_breakpoint(bp);
if (IS_ERR(tmp))
return PTR_ERR(tmp);

list_add(&tmp->list, &task_bps);
return 0;
}

static void task_bps_remove(struct perf_event *bp)
{
struct list_head *pos, *q;

list_for_each_safe(pos, q, &task_bps) {
struct breakpoint *tmp = list_entry(pos, struct breakpoint, list);

if (tmp->bp == bp) {
list_del(&tmp->list);
kfree(tmp);
break;
}
}
}

/*
* If any task has breakpoint from alternate infrastructure,
* return true. Otherwise return false.
*/
static bool all_task_bps_check(struct perf_event *bp)
{
struct breakpoint *tmp;

list_for_each_entry(tmp, &task_bps, list) {
if (!can_co_exist(tmp, bp))
return true;
}
return false;
}

/*
* If same task has breakpoint from alternate infrastructure,
* return true. Otherwise return false.
*/
static bool same_task_bps_check(struct perf_event *bp)
{
struct breakpoint *tmp;

list_for_each_entry(tmp, &task_bps, list) {
if (tmp->bp->hw.target == bp->hw.target &&
!can_co_exist(tmp, bp))
return true;
}
return false;
}

static int cpu_bps_add(struct perf_event *bp)
{
struct breakpoint **cpu_bp;
struct breakpoint *tmp;
int i = 0;

tmp = alloc_breakpoint(bp);
if (IS_ERR(tmp))
return PTR_ERR(tmp);

cpu_bp = per_cpu_ptr(cpu_bps, bp->cpu);
for (i = 0; i < nr_wp_slots(); i++) {
if (!cpu_bp[i]) {
cpu_bp[i] = tmp;
break;
}
}
return 0;
}

static void cpu_bps_remove(struct perf_event *bp)
{
struct breakpoint **cpu_bp;
int i = 0;

cpu_bp = per_cpu_ptr(cpu_bps, bp->cpu);
for (i = 0; i < nr_wp_slots(); i++) {
if (!cpu_bp[i])
continue;

if (cpu_bp[i]->bp == bp) {
kfree(cpu_bp[i]);
cpu_bp[i] = NULL;
break;
}
}
}

static bool cpu_bps_check(int cpu, struct perf_event *bp)
{
struct breakpoint **cpu_bp;
int i;

cpu_bp = per_cpu_ptr(cpu_bps, cpu);
for (i = 0; i < nr_wp_slots(); i++) {
if (cpu_bp[i] && !can_co_exist(cpu_bp[i], bp))
return true;
}
return false;
}

static bool all_cpu_bps_check(struct perf_event *bp)
{
int cpu;

for_each_online_cpu(cpu) {
if (cpu_bps_check(cpu, bp))
return true;
}
return false;
}

/*
* We don't use any locks to serialize accesses to cpu_bps or task_bps
* because are already inside nr_bp_mutex.
*/
int arch_reserve_bp_slot(struct perf_event *bp)
{
int ret;

/* ptrace breakpoint */
if (is_ptrace_bp(bp)) {
if (all_cpu_bps_check(bp))
return -ENOSPC;

if (same_task_bps_check(bp))
return -ENOSPC;

return task_bps_add(bp);
}

/* perf breakpoint */
if (is_kernel_addr(bp->attr.bp_addr))
return 0;

if (bp->hw.target && bp->cpu == -1) {
if (same_task_bps_check(bp))
return -ENOSPC;

return task_bps_add(bp);
} else if (!bp->hw.target && bp->cpu != -1) {
if (all_task_bps_check(bp))
return -ENOSPC;

return cpu_bps_add(bp);
}

if (same_task_bps_check(bp))
return -ENOSPC;

ret = cpu_bps_add(bp);
if (ret)
return ret;
ret = task_bps_add(bp);
if (ret)
cpu_bps_remove(bp);

return ret;
}

void arch_release_bp_slot(struct perf_event *bp)
{
if (!is_kernel_addr(bp->attr.bp_addr)) {
if (bp->hw.target)
task_bps_remove(bp);
if (bp->cpu != -1)
cpu_bps_remove(bp);
}
}

/*
* Perform cleanup of arch-specific counters during unregistration
* of the perf-event
Expand Down
16 changes: 16 additions & 0 deletions kernel/events/hw_breakpoint.c
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,15 @@ toggle_bp_slot(struct perf_event *bp, bool enable, enum bp_type_idx type,
list_del(&bp->hw.bp_list);
}

__weak int arch_reserve_bp_slot(struct perf_event *bp)
{
return 0;
}

__weak void arch_release_bp_slot(struct perf_event *bp)
{
}

/*
* Function to perform processor-specific cleanup during unregistration
*/
Expand Down Expand Up @@ -270,6 +279,7 @@ static int __reserve_bp_slot(struct perf_event *bp, u64 bp_type)
struct bp_busy_slots slots = {0};
enum bp_type_idx type;
int weight;
int ret;

/* We couldn't initialize breakpoint constraints on boot */
if (!constraints_initialized)
Expand All @@ -294,6 +304,10 @@ static int __reserve_bp_slot(struct perf_event *bp, u64 bp_type)
if (slots.pinned + (!!slots.flexible) > nr_slots[type])
return -ENOSPC;

ret = arch_reserve_bp_slot(bp);
if (ret)
return ret;

toggle_bp_slot(bp, true, type, weight);

return 0;
Expand All @@ -317,6 +331,8 @@ static void __release_bp_slot(struct perf_event *bp, u64 bp_type)
enum bp_type_idx type;
int weight;

arch_release_bp_slot(bp);

type = find_slot_idx(bp_type);
weight = hw_breakpoint_weight(bp);
toggle_bp_slot(bp, false, type, weight);
Expand Down

0 comments on commit 29da4f9

Please sign in to comment.