| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Author: Hanlu Li <lihanlu@loongson.cn> |
| * Huacai Chen <chenhuacai@loongson.cn> |
| * |
| * Copyright (C) 2020-2022 Loongson Technology Corporation Limited |
| * |
| * Derived from MIPS: |
| * Copyright (C) 1992 Ross Biro |
| * Copyright (C) Linus Torvalds |
| * Copyright (C) 1994, 95, 96, 97, 98, 2000 Ralf Baechle |
| * Copyright (C) 1996 David S. Miller |
| * Kevin D. Kissell, kevink@mips.com and Carsten Langgaard, carstenl@mips.com |
| * Copyright (C) 1999 MIPS Technologies, Inc. |
| * Copyright (C) 2000 Ulf Carlsson |
| */ |
| #include <linux/kernel.h> |
| #include <linux/audit.h> |
| #include <linux/compiler.h> |
| #include <linux/context_tracking.h> |
| #include <linux/elf.h> |
| #include <linux/errno.h> |
| #include <linux/hw_breakpoint.h> |
| #include <linux/mm.h> |
| #include <linux/nospec.h> |
| #include <linux/ptrace.h> |
| #include <linux/regset.h> |
| #include <linux/sched.h> |
| #include <linux/sched/task_stack.h> |
| #include <linux/security.h> |
| #include <linux/smp.h> |
| #include <linux/stddef.h> |
| #include <linux/seccomp.h> |
| #include <linux/thread_info.h> |
| #include <linux/uaccess.h> |
| |
| #include <asm/byteorder.h> |
| #include <asm/cpu.h> |
| #include <asm/cpu-info.h> |
| #include <asm/fpu.h> |
| #include <asm/lbt.h> |
| #include <asm/loongarch.h> |
| #include <asm/page.h> |
| #include <asm/pgtable.h> |
| #include <asm/processor.h> |
| #include <asm/ptrace.h> |
| #include <asm/reg.h> |
| #include <asm/syscall.h> |
| |
| static void init_fp_ctx(struct task_struct *target) |
| { |
| /* The target already has context */ |
| if (tsk_used_math(target)) |
| return; |
| |
| /* Begin with data registers set to all 1s... */ |
| memset(&target->thread.fpu.fpr, ~0, sizeof(target->thread.fpu.fpr)); |
| set_stopped_child_used_math(target); |
| } |
| |
| /* |
| * Called by kernel/ptrace.c when detaching.. |
| * |
| * Make sure single step bits etc are not set. |
| */ |
| void ptrace_disable(struct task_struct *child) |
| { |
| /* Don't load the watchpoint registers for the ex-child. */ |
| clear_tsk_thread_flag(child, TIF_LOAD_WATCH); |
| clear_tsk_thread_flag(child, TIF_SINGLESTEP); |
| } |
| |
| /* regset get/set implementations */ |
| |
| static int gpr_get(struct task_struct *target, |
| const struct user_regset *regset, |
| struct membuf to) |
| { |
| int r; |
| struct pt_regs *regs = task_pt_regs(target); |
| |
| r = membuf_write(&to, ®s->regs, sizeof(u64) * GPR_NUM); |
| r = membuf_write(&to, ®s->orig_a0, sizeof(u64)); |
| r = membuf_write(&to, ®s->csr_era, sizeof(u64)); |
| r = membuf_write(&to, ®s->csr_badvaddr, sizeof(u64)); |
| |
| return r; |
| } |
| |
| static int gpr_set(struct task_struct *target, |
| const struct user_regset *regset, |
| unsigned int pos, unsigned int count, |
| const void *kbuf, const void __user *ubuf) |
| { |
| int err; |
| int a0_start = sizeof(u64) * GPR_NUM; |
| int era_start = a0_start + sizeof(u64); |
| int badvaddr_start = era_start + sizeof(u64); |
| struct pt_regs *regs = task_pt_regs(target); |
| |
| err = user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| ®s->regs, |
| 0, a0_start); |
| err |= user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| ®s->orig_a0, |
| a0_start, a0_start + sizeof(u64)); |
| err |= user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| ®s->csr_era, |
| era_start, era_start + sizeof(u64)); |
| err |= user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| ®s->csr_badvaddr, |
| badvaddr_start, badvaddr_start + sizeof(u64)); |
| |
| return err; |
| } |
| |
| |
| /* |
| * Get the general floating-point registers. |
| */ |
| static int gfpr_get(struct task_struct *target, struct membuf *to) |
| { |
| return membuf_write(to, &target->thread.fpu.fpr, |
| sizeof(elf_fpreg_t) * NUM_FPU_REGS); |
| } |
| |
| static int gfpr_get_simd(struct task_struct *target, struct membuf *to) |
| { |
| int i, r; |
| u64 fpr_val; |
| |
| BUILD_BUG_ON(sizeof(fpr_val) != sizeof(elf_fpreg_t)); |
| for (i = 0; i < NUM_FPU_REGS; i++) { |
| fpr_val = get_fpr64(&target->thread.fpu.fpr[i], 0); |
| r = membuf_write(to, &fpr_val, sizeof(elf_fpreg_t)); |
| } |
| |
| return r; |
| } |
| |
| /* |
| * Choose the appropriate helper for general registers, and then copy |
| * the FCC and FCSR registers separately. |
| */ |
| static int fpr_get(struct task_struct *target, |
| const struct user_regset *regset, |
| struct membuf to) |
| { |
| int r; |
| |
| save_fpu_regs(target); |
| |
| if (sizeof(target->thread.fpu.fpr[0]) == sizeof(elf_fpreg_t)) |
| r = gfpr_get(target, &to); |
| else |
| r = gfpr_get_simd(target, &to); |
| |
| r = membuf_write(&to, &target->thread.fpu.fcc, sizeof(target->thread.fpu.fcc)); |
| r = membuf_write(&to, &target->thread.fpu.fcsr, sizeof(target->thread.fpu.fcsr)); |
| |
| return r; |
| } |
| |
| static int gfpr_set(struct task_struct *target, |
| unsigned int *pos, unsigned int *count, |
| const void **kbuf, const void __user **ubuf) |
| { |
| return user_regset_copyin(pos, count, kbuf, ubuf, |
| &target->thread.fpu.fpr, |
| 0, NUM_FPU_REGS * sizeof(elf_fpreg_t)); |
| } |
| |
| static int gfpr_set_simd(struct task_struct *target, |
| unsigned int *pos, unsigned int *count, |
| const void **kbuf, const void __user **ubuf) |
| { |
| int i, err; |
| u64 fpr_val; |
| |
| BUILD_BUG_ON(sizeof(fpr_val) != sizeof(elf_fpreg_t)); |
| for (i = 0; i < NUM_FPU_REGS && *count > 0; i++) { |
| err = user_regset_copyin(pos, count, kbuf, ubuf, |
| &fpr_val, i * sizeof(elf_fpreg_t), |
| (i + 1) * sizeof(elf_fpreg_t)); |
| if (err) |
| return err; |
| set_fpr64(&target->thread.fpu.fpr[i], 0, fpr_val); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Choose the appropriate helper for general registers, and then copy |
| * the FCC register separately. |
| */ |
| static int fpr_set(struct task_struct *target, |
| const struct user_regset *regset, |
| unsigned int pos, unsigned int count, |
| const void *kbuf, const void __user *ubuf) |
| { |
| const int fcc_start = NUM_FPU_REGS * sizeof(elf_fpreg_t); |
| const int fcsr_start = fcc_start + sizeof(u64); |
| int err; |
| |
| BUG_ON(count % sizeof(elf_fpreg_t)); |
| if (pos + count > sizeof(elf_fpregset_t)) |
| return -EIO; |
| |
| init_fp_ctx(target); |
| |
| if (sizeof(target->thread.fpu.fpr[0]) == sizeof(elf_fpreg_t)) |
| err = gfpr_set(target, &pos, &count, &kbuf, &ubuf); |
| else |
| err = gfpr_set_simd(target, &pos, &count, &kbuf, &ubuf); |
| if (err) |
| return err; |
| |
| err |= user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| &target->thread.fpu.fcc, fcc_start, |
| fcc_start + sizeof(u64)); |
| err |= user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| &target->thread.fpu.fcsr, fcsr_start, |
| fcsr_start + sizeof(u32)); |
| |
| return err; |
| } |
| |
| static int cfg_get(struct task_struct *target, |
| const struct user_regset *regset, |
| struct membuf to) |
| { |
| int i, r; |
| u32 cfg_val; |
| |
| i = 0; |
| while (to.left > 0) { |
| cfg_val = read_cpucfg(i++); |
| r = membuf_write(&to, &cfg_val, sizeof(u32)); |
| } |
| |
| return r; |
| } |
| |
| /* |
| * CFG registers are read-only. |
| */ |
| static int cfg_set(struct task_struct *target, |
| const struct user_regset *regset, |
| unsigned int pos, unsigned int count, |
| const void *kbuf, const void __user *ubuf) |
| { |
| return 0; |
| } |
| |
| #ifdef CONFIG_CPU_HAS_LSX |
| |
| static void copy_pad_fprs(struct task_struct *target, |
| const struct user_regset *regset, |
| struct membuf *to, unsigned int live_sz) |
| { |
| int i, j; |
| unsigned long long fill = ~0ull; |
| unsigned int cp_sz, pad_sz; |
| |
| cp_sz = min(regset->size, live_sz); |
| pad_sz = regset->size - cp_sz; |
| WARN_ON(pad_sz % sizeof(fill)); |
| |
| for (i = 0; i < NUM_FPU_REGS; i++) { |
| membuf_write(to, &target->thread.fpu.fpr[i], cp_sz); |
| for (j = 0; j < (pad_sz / sizeof(fill)); j++) { |
| membuf_store(to, fill); |
| } |
| } |
| } |
| |
| static int simd_get(struct task_struct *target, |
| const struct user_regset *regset, |
| struct membuf to) |
| { |
| const unsigned int wr_size = NUM_FPU_REGS * regset->size; |
| |
| save_fpu_regs(target); |
| |
| if (!tsk_used_math(target)) { |
| /* The task hasn't used FP or LSX, fill with 0xff */ |
| copy_pad_fprs(target, regset, &to, 0); |
| } else if (!test_tsk_thread_flag(target, TIF_LSX_CTX_LIVE)) { |
| /* Copy scalar FP context, fill the rest with 0xff */ |
| copy_pad_fprs(target, regset, &to, 8); |
| #ifdef CONFIG_CPU_HAS_LASX |
| } else if (!test_tsk_thread_flag(target, TIF_LASX_CTX_LIVE)) { |
| /* Copy LSX 128 Bit context, fill the rest with 0xff */ |
| copy_pad_fprs(target, regset, &to, 16); |
| #endif |
| } else if (sizeof(target->thread.fpu.fpr[0]) == regset->size) { |
| /* Trivially copy the vector registers */ |
| membuf_write(&to, &target->thread.fpu.fpr, wr_size); |
| } else { |
| /* Copy as much context as possible, fill the rest with 0xff */ |
| copy_pad_fprs(target, regset, &to, sizeof(target->thread.fpu.fpr[0])); |
| } |
| |
| return 0; |
| } |
| |
| static int simd_set(struct task_struct *target, |
| const struct user_regset *regset, |
| unsigned int pos, unsigned int count, |
| const void *kbuf, const void __user *ubuf) |
| { |
| const unsigned int wr_size = NUM_FPU_REGS * regset->size; |
| unsigned int cp_sz; |
| int i, err, start; |
| |
| init_fp_ctx(target); |
| |
| if (sizeof(target->thread.fpu.fpr[0]) == regset->size) { |
| /* Trivially copy the vector registers */ |
| err = user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| &target->thread.fpu.fpr, |
| 0, wr_size); |
| } else { |
| /* Copy as much context as possible */ |
| cp_sz = min_t(unsigned int, regset->size, |
| sizeof(target->thread.fpu.fpr[0])); |
| |
| i = start = err = 0; |
| for (; i < NUM_FPU_REGS; i++, start += regset->size) { |
| err |= user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| &target->thread.fpu.fpr[i], |
| start, start + cp_sz); |
| } |
| } |
| |
| return err; |
| } |
| |
| #endif /* CONFIG_CPU_HAS_LSX */ |
| |
| #ifdef CONFIG_CPU_HAS_LBT |
| static int lbt_get(struct task_struct *target, |
| const struct user_regset *regset, |
| struct membuf to) |
| { |
| int r; |
| |
| r = membuf_write(&to, &target->thread.lbt.scr0, sizeof(target->thread.lbt.scr0)); |
| r = membuf_write(&to, &target->thread.lbt.scr1, sizeof(target->thread.lbt.scr1)); |
| r = membuf_write(&to, &target->thread.lbt.scr2, sizeof(target->thread.lbt.scr2)); |
| r = membuf_write(&to, &target->thread.lbt.scr3, sizeof(target->thread.lbt.scr3)); |
| r = membuf_write(&to, &target->thread.lbt.eflags, sizeof(u32)); |
| r = membuf_write(&to, &target->thread.fpu.ftop, sizeof(u32)); |
| |
| return r; |
| } |
| |
| static int lbt_set(struct task_struct *target, |
| const struct user_regset *regset, |
| unsigned int pos, unsigned int count, |
| const void *kbuf, const void __user *ubuf) |
| { |
| int err = 0; |
| const int eflags_start = 4 * sizeof(target->thread.lbt.scr0); |
| const int ftop_start = eflags_start + sizeof(u32); |
| |
| err |= user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| &target->thread.lbt.scr0, |
| 0, 4 * sizeof(target->thread.lbt.scr0)); |
| err |= user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| &target->thread.lbt.eflags, |
| eflags_start, ftop_start); |
| err |= user_regset_copyin(&pos, &count, &kbuf, &ubuf, |
| &target->thread.fpu.ftop, |
| ftop_start, ftop_start + sizeof(u32)); |
| |
| return err; |
| } |
| #endif /* CONFIG_CPU_HAS_LBT */ |
| |
| #ifdef CONFIG_HAVE_HW_BREAKPOINT |
| |
| /* |
| * Handle hitting a HW-breakpoint. |
| */ |
| static void ptrace_hbptriggered(struct perf_event *bp, |
| struct perf_sample_data *data, |
| struct pt_regs *regs) |
| { |
| int i; |
| struct arch_hw_breakpoint *bkpt = counter_arch_bp(bp); |
| |
| for (i = 0; i < LOONGARCH_MAX_BRP; ++i) |
| if (current->thread.hbp_break[i] == bp) |
| break; |
| |
| for (i = 0; i < LOONGARCH_MAX_WRP; ++i) |
| if (current->thread.hbp_watch[i] == bp) |
| break; |
| |
| force_sig_ptrace_errno_trap(i, (void __user *)bkpt->address); |
| } |
| |
| static struct perf_event *ptrace_hbp_get_event(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx) |
| { |
| struct perf_event *bp; |
| |
| switch (note_type) { |
| case NT_LOONGARCH_HW_BREAK: |
| if (idx >= LOONGARCH_MAX_BRP) |
| return ERR_PTR(-EINVAL); |
| idx = array_index_nospec(idx, LOONGARCH_MAX_BRP); |
| bp = tsk->thread.hbp_break[idx]; |
| break; |
| case NT_LOONGARCH_HW_WATCH: |
| if (idx >= LOONGARCH_MAX_WRP) |
| return ERR_PTR(-EINVAL); |
| idx = array_index_nospec(idx, LOONGARCH_MAX_WRP); |
| bp = tsk->thread.hbp_watch[idx]; |
| break; |
| } |
| |
| return bp; |
| } |
| |
| static int ptrace_hbp_set_event(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx, |
| struct perf_event *bp) |
| { |
| switch (note_type) { |
| case NT_LOONGARCH_HW_BREAK: |
| if (idx >= LOONGARCH_MAX_BRP) |
| return -EINVAL; |
| idx = array_index_nospec(idx, LOONGARCH_MAX_BRP); |
| tsk->thread.hbp_break[idx] = bp; |
| break; |
| case NT_LOONGARCH_HW_WATCH: |
| if (idx >= LOONGARCH_MAX_WRP) |
| return -EINVAL; |
| idx = array_index_nospec(idx, LOONGARCH_MAX_WRP); |
| tsk->thread.hbp_watch[idx] = bp; |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static struct perf_event *ptrace_hbp_create(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx) |
| { |
| int err, type; |
| struct perf_event *bp; |
| struct perf_event_attr attr; |
| |
| switch (note_type) { |
| case NT_LOONGARCH_HW_BREAK: |
| type = HW_BREAKPOINT_X; |
| break; |
| case NT_LOONGARCH_HW_WATCH: |
| type = HW_BREAKPOINT_RW; |
| break; |
| default: |
| return ERR_PTR(-EINVAL); |
| } |
| |
| ptrace_breakpoint_init(&attr); |
| |
| /* |
| * Initialise fields to sane defaults |
| * (i.e. values that will pass validation). |
| */ |
| attr.bp_addr = 0; |
| attr.bp_len = HW_BREAKPOINT_LEN_4; |
| attr.bp_type = type; |
| attr.disabled = 1; |
| |
| bp = register_user_hw_breakpoint(&attr, ptrace_hbptriggered, NULL, tsk); |
| if (IS_ERR(bp)) |
| return bp; |
| |
| err = ptrace_hbp_set_event(note_type, tsk, idx, bp); |
| if (err) |
| return ERR_PTR(err); |
| |
| return bp; |
| } |
| |
| static int ptrace_hbp_fill_attr_ctrl(unsigned int note_type, |
| struct arch_hw_breakpoint_ctrl ctrl, |
| struct perf_event_attr *attr) |
| { |
| int err, len, type, offset; |
| |
| err = arch_bp_generic_fields(ctrl, &len, &type, &offset); |
| if (err) |
| return err; |
| |
| switch (note_type) { |
| case NT_LOONGARCH_HW_BREAK: |
| if ((type & HW_BREAKPOINT_X) != type) |
| return -EINVAL; |
| break; |
| case NT_LOONGARCH_HW_WATCH: |
| if ((type & HW_BREAKPOINT_RW) != type) |
| return -EINVAL; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| attr->bp_len = len; |
| attr->bp_type = type; |
| attr->bp_addr += offset; |
| |
| return 0; |
| } |
| |
| static int ptrace_hbp_get_resource_info(unsigned int note_type, u64 *info) |
| { |
| u8 num; |
| u64 reg = 0; |
| |
| switch (note_type) { |
| case NT_LOONGARCH_HW_BREAK: |
| num = hw_breakpoint_slots(TYPE_INST); |
| break; |
| case NT_LOONGARCH_HW_WATCH: |
| num = hw_breakpoint_slots(TYPE_DATA); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| *info = reg | num; |
| |
| return 0; |
| } |
| |
| static struct perf_event *ptrace_hbp_get_initialised_bp(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx) |
| { |
| struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx); |
| |
| if (!bp) |
| bp = ptrace_hbp_create(note_type, tsk, idx); |
| |
| return bp; |
| } |
| |
| static int ptrace_hbp_get_ctrl(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx, u32 *ctrl) |
| { |
| struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx); |
| |
| if (IS_ERR(bp)) |
| return PTR_ERR(bp); |
| |
| *ctrl = bp ? encode_ctrl_reg(counter_arch_bp(bp)->ctrl) : 0; |
| |
| return 0; |
| } |
| |
| static int ptrace_hbp_get_mask(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx, u64 *mask) |
| { |
| struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx); |
| |
| if (IS_ERR(bp)) |
| return PTR_ERR(bp); |
| |
| *mask = bp ? counter_arch_bp(bp)->mask : 0; |
| |
| return 0; |
| } |
| |
| static int ptrace_hbp_get_addr(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx, u64 *addr) |
| { |
| struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx); |
| |
| if (IS_ERR(bp)) |
| return PTR_ERR(bp); |
| |
| *addr = bp ? counter_arch_bp(bp)->address : 0; |
| |
| return 0; |
| } |
| |
| static int ptrace_hbp_set_ctrl(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx, u32 uctrl) |
| { |
| int err; |
| struct perf_event *bp; |
| struct perf_event_attr attr; |
| struct arch_hw_breakpoint_ctrl ctrl; |
| |
| bp = ptrace_hbp_get_initialised_bp(note_type, tsk, idx); |
| if (IS_ERR(bp)) |
| return PTR_ERR(bp); |
| |
| attr = bp->attr; |
| decode_ctrl_reg(uctrl, &ctrl); |
| err = ptrace_hbp_fill_attr_ctrl(note_type, ctrl, &attr); |
| if (err) |
| return err; |
| |
| return modify_user_hw_breakpoint(bp, &attr); |
| } |
| |
| static int ptrace_hbp_set_mask(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx, u64 mask) |
| { |
| struct perf_event *bp; |
| struct perf_event_attr attr; |
| struct arch_hw_breakpoint *info; |
| |
| bp = ptrace_hbp_get_initialised_bp(note_type, tsk, idx); |
| if (IS_ERR(bp)) |
| return PTR_ERR(bp); |
| |
| attr = bp->attr; |
| info = counter_arch_bp(bp); |
| info->mask = mask; |
| |
| return modify_user_hw_breakpoint(bp, &attr); |
| } |
| |
| static int ptrace_hbp_set_addr(unsigned int note_type, |
| struct task_struct *tsk, |
| unsigned long idx, u64 addr) |
| { |
| struct perf_event *bp; |
| struct perf_event_attr attr; |
| |
| bp = ptrace_hbp_get_initialised_bp(note_type, tsk, idx); |
| if (IS_ERR(bp)) |
| return PTR_ERR(bp); |
| |
| attr = bp->attr; |
| attr.bp_addr = addr; |
| |
| return modify_user_hw_breakpoint(bp, &attr); |
| } |
| |
| #define PTRACE_HBP_ADDR_SZ sizeof(u64) |
| #define PTRACE_HBP_MASK_SZ sizeof(u64) |
| #define PTRACE_HBP_CTRL_SZ sizeof(u32) |
| #define PTRACE_HBP_PAD_SZ sizeof(u32) |
| |
| static int hw_break_get(struct task_struct *target, |
| const struct user_regset *regset, |
| struct membuf to) |
| { |
| u64 info; |
| u32 ctrl; |
| u64 addr, mask; |
| int ret, idx = 0; |
| unsigned int note_type = regset->core_note_type; |
| |
| /* Resource info */ |
| ret = ptrace_hbp_get_resource_info(note_type, &info); |
| if (ret) |
| return ret; |
| |
| membuf_write(&to, &info, sizeof(info)); |
| |
| /* (address, mask, ctrl) registers */ |
| while (to.left) { |
| ret = ptrace_hbp_get_addr(note_type, target, idx, &addr); |
| if (ret) |
| return ret; |
| |
| ret = ptrace_hbp_get_mask(note_type, target, idx, &mask); |
| if (ret) |
| return ret; |
| |
| ret = ptrace_hbp_get_ctrl(note_type, target, idx, &ctrl); |
| if (ret) |
| return ret; |
| |
| membuf_store(&to, addr); |
| membuf_store(&to, mask); |
| membuf_store(&to, ctrl); |
| membuf_zero(&to, sizeof(u32)); |
| idx++; |
| } |
| |
| return 0; |
| } |
| |
| static int hw_break_set(struct task_struct *target, |
| const struct user_regset *regset, |
| unsigned int pos, unsigned int count, |
| const void *kbuf, const void __user *ubuf) |
| { |
| u32 ctrl; |
| u64 addr, mask; |
| int ret, idx = 0, offset, limit; |
| unsigned int note_type = regset->core_note_type; |
| |
| /* Resource info */ |
| offset = offsetof(struct user_watch_state, dbg_regs); |
| user_regset_copyin_ignore(&pos, &count, &kbuf, &ubuf, 0, offset); |
| |
| /* (address, mask, ctrl) registers */ |
| limit = regset->n * regset->size; |
| while (count && offset < limit) { |
| if (count < PTRACE_HBP_ADDR_SZ) |
| return -EINVAL; |
| |
| ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &addr, |
| offset, offset + PTRACE_HBP_ADDR_SZ); |
| if (ret) |
| return ret; |
| |
| ret = ptrace_hbp_set_addr(note_type, target, idx, addr); |
| if (ret) |
| return ret; |
| offset += PTRACE_HBP_ADDR_SZ; |
| |
| if (!count) |
| break; |
| |
| ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &mask, |
| offset, offset + PTRACE_HBP_MASK_SZ); |
| if (ret) |
| return ret; |
| |
| ret = ptrace_hbp_set_mask(note_type, target, idx, mask); |
| if (ret) |
| return ret; |
| offset += PTRACE_HBP_MASK_SZ; |
| |
| ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &ctrl, |
| offset, offset + PTRACE_HBP_CTRL_SZ); |
| if (ret) |
| return ret; |
| |
| ret = ptrace_hbp_set_ctrl(note_type, target, idx, ctrl); |
| if (ret) |
| return ret; |
| offset += PTRACE_HBP_CTRL_SZ; |
| |
| user_regset_copyin_ignore(&pos, &count, &kbuf, &ubuf, |
| offset, offset + PTRACE_HBP_PAD_SZ); |
| offset += PTRACE_HBP_PAD_SZ; |
| |
| idx++; |
| } |
| |
| return 0; |
| } |
| |
| #endif |
| |
| struct pt_regs_offset { |
| const char *name; |
| int offset; |
| }; |
| |
| #define REG_OFFSET_NAME(n, r) {.name = #n, .offset = offsetof(struct pt_regs, r)} |
| #define REG_OFFSET_END {.name = NULL, .offset = 0} |
| |
| static const struct pt_regs_offset regoffset_table[] = { |
| REG_OFFSET_NAME(r0, regs[0]), |
| REG_OFFSET_NAME(r1, regs[1]), |
| REG_OFFSET_NAME(r2, regs[2]), |
| REG_OFFSET_NAME(r3, regs[3]), |
| REG_OFFSET_NAME(r4, regs[4]), |
| REG_OFFSET_NAME(r5, regs[5]), |
| REG_OFFSET_NAME(r6, regs[6]), |
| REG_OFFSET_NAME(r7, regs[7]), |
| REG_OFFSET_NAME(r8, regs[8]), |
| REG_OFFSET_NAME(r9, regs[9]), |
| REG_OFFSET_NAME(r10, regs[10]), |
| REG_OFFSET_NAME(r11, regs[11]), |
| REG_OFFSET_NAME(r12, regs[12]), |
| REG_OFFSET_NAME(r13, regs[13]), |
| REG_OFFSET_NAME(r14, regs[14]), |
| REG_OFFSET_NAME(r15, regs[15]), |
| REG_OFFSET_NAME(r16, regs[16]), |
| REG_OFFSET_NAME(r17, regs[17]), |
| REG_OFFSET_NAME(r18, regs[18]), |
| REG_OFFSET_NAME(r19, regs[19]), |
| REG_OFFSET_NAME(r20, regs[20]), |
| REG_OFFSET_NAME(r21, regs[21]), |
| REG_OFFSET_NAME(r22, regs[22]), |
| REG_OFFSET_NAME(r23, regs[23]), |
| REG_OFFSET_NAME(r24, regs[24]), |
| REG_OFFSET_NAME(r25, regs[25]), |
| REG_OFFSET_NAME(r26, regs[26]), |
| REG_OFFSET_NAME(r27, regs[27]), |
| REG_OFFSET_NAME(r28, regs[28]), |
| REG_OFFSET_NAME(r29, regs[29]), |
| REG_OFFSET_NAME(r30, regs[30]), |
| REG_OFFSET_NAME(r31, regs[31]), |
| REG_OFFSET_NAME(orig_a0, orig_a0), |
| REG_OFFSET_NAME(csr_era, csr_era), |
| REG_OFFSET_NAME(csr_badvaddr, csr_badvaddr), |
| REG_OFFSET_NAME(csr_crmd, csr_crmd), |
| REG_OFFSET_NAME(csr_prmd, csr_prmd), |
| REG_OFFSET_NAME(csr_euen, csr_euen), |
| REG_OFFSET_NAME(csr_ecfg, csr_ecfg), |
| REG_OFFSET_NAME(csr_estat, csr_estat), |
| REG_OFFSET_END, |
| }; |
| |
| /** |
| * regs_query_register_offset() - query register offset from its name |
| * @name: the name of a register |
| * |
| * regs_query_register_offset() returns the offset of a register in struct |
| * pt_regs from its name. If the name is invalid, this returns -EINVAL; |
| */ |
| int regs_query_register_offset(const char *name) |
| { |
| const struct pt_regs_offset *roff; |
| |
| for (roff = regoffset_table; roff->name != NULL; roff++) |
| if (!strcmp(roff->name, name)) |
| return roff->offset; |
| return -EINVAL; |
| } |
| |
| enum loongarch_regset { |
| REGSET_GPR, |
| REGSET_FPR, |
| REGSET_CPUCFG, |
| #ifdef CONFIG_CPU_HAS_LSX |
| REGSET_LSX, |
| #endif |
| #ifdef CONFIG_CPU_HAS_LASX |
| REGSET_LASX, |
| #endif |
| #ifdef CONFIG_CPU_HAS_LBT |
| REGSET_LBT, |
| #endif |
| #ifdef CONFIG_HAVE_HW_BREAKPOINT |
| REGSET_HW_BREAK, |
| REGSET_HW_WATCH, |
| #endif |
| }; |
| |
| static const struct user_regset loongarch64_regsets[] = { |
| [REGSET_GPR] = { |
| .core_note_type = NT_PRSTATUS, |
| .n = ELF_NGREG, |
| .size = sizeof(elf_greg_t), |
| .align = sizeof(elf_greg_t), |
| .regset_get = gpr_get, |
| .set = gpr_set, |
| }, |
| [REGSET_FPR] = { |
| .core_note_type = NT_PRFPREG, |
| .n = ELF_NFPREG, |
| .size = sizeof(elf_fpreg_t), |
| .align = sizeof(elf_fpreg_t), |
| .regset_get = fpr_get, |
| .set = fpr_set, |
| }, |
| [REGSET_CPUCFG] = { |
| .core_note_type = NT_LOONGARCH_CPUCFG, |
| .n = 64, |
| .size = sizeof(u32), |
| .align = sizeof(u32), |
| .regset_get = cfg_get, |
| .set = cfg_set, |
| }, |
| #ifdef CONFIG_CPU_HAS_LSX |
| [REGSET_LSX] = { |
| .core_note_type = NT_LOONGARCH_LSX, |
| .n = NUM_FPU_REGS, |
| .size = 16, |
| .align = 16, |
| .regset_get = simd_get, |
| .set = simd_set, |
| }, |
| #endif |
| #ifdef CONFIG_CPU_HAS_LASX |
| [REGSET_LASX] = { |
| .core_note_type = NT_LOONGARCH_LASX, |
| .n = NUM_FPU_REGS, |
| .size = 32, |
| .align = 32, |
| .regset_get = simd_get, |
| .set = simd_set, |
| }, |
| #endif |
| #ifdef CONFIG_CPU_HAS_LBT |
| [REGSET_LBT] = { |
| .core_note_type = NT_LOONGARCH_LBT, |
| .n = 5, |
| .size = sizeof(u64), |
| .align = sizeof(u64), |
| .regset_get = lbt_get, |
| .set = lbt_set, |
| }, |
| #endif |
| #ifdef CONFIG_HAVE_HW_BREAKPOINT |
| [REGSET_HW_BREAK] = { |
| .core_note_type = NT_LOONGARCH_HW_BREAK, |
| .n = sizeof(struct user_watch_state) / sizeof(u32), |
| .size = sizeof(u32), |
| .align = sizeof(u32), |
| .regset_get = hw_break_get, |
| .set = hw_break_set, |
| }, |
| [REGSET_HW_WATCH] = { |
| .core_note_type = NT_LOONGARCH_HW_WATCH, |
| .n = sizeof(struct user_watch_state) / sizeof(u32), |
| .size = sizeof(u32), |
| .align = sizeof(u32), |
| .regset_get = hw_break_get, |
| .set = hw_break_set, |
| }, |
| #endif |
| }; |
| |
| static const struct user_regset_view user_loongarch64_view = { |
| .name = "loongarch64", |
| .e_machine = ELF_ARCH, |
| .regsets = loongarch64_regsets, |
| .n = ARRAY_SIZE(loongarch64_regsets), |
| }; |
| |
| |
| const struct user_regset_view *task_user_regset_view(struct task_struct *task) |
| { |
| return &user_loongarch64_view; |
| } |
| |
| static inline int read_user(struct task_struct *target, unsigned long addr, |
| unsigned long __user *data) |
| { |
| unsigned long tmp = 0; |
| |
| switch (addr) { |
| case 0 ... 31: |
| tmp = task_pt_regs(target)->regs[addr]; |
| break; |
| case ARG0: |
| tmp = task_pt_regs(target)->orig_a0; |
| break; |
| case PC: |
| tmp = task_pt_regs(target)->csr_era; |
| break; |
| case BADVADDR: |
| tmp = task_pt_regs(target)->csr_badvaddr; |
| break; |
| default: |
| return -EIO; |
| } |
| |
| return put_user(tmp, data); |
| } |
| |
| static inline int write_user(struct task_struct *target, unsigned long addr, |
| unsigned long data) |
| { |
| switch (addr) { |
| case 0 ... 31: |
| task_pt_regs(target)->regs[addr] = data; |
| break; |
| case ARG0: |
| task_pt_regs(target)->orig_a0 = data; |
| break; |
| case PC: |
| task_pt_regs(target)->csr_era = data; |
| break; |
| case BADVADDR: |
| task_pt_regs(target)->csr_badvaddr = data; |
| break; |
| default: |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| long arch_ptrace(struct task_struct *child, long request, |
| unsigned long addr, unsigned long data) |
| { |
| int ret; |
| unsigned long __user *datap = (void __user *) data; |
| |
| switch (request) { |
| case PTRACE_PEEKUSR: |
| ret = read_user(child, addr, datap); |
| break; |
| |
| case PTRACE_POKEUSR: |
| ret = write_user(child, addr, data); |
| break; |
| |
| default: |
| ret = ptrace_request(child, request, addr, data); |
| break; |
| } |
| |
| return ret; |
| } |
| |
| #ifdef CONFIG_HAVE_HW_BREAKPOINT |
| static void ptrace_triggered(struct perf_event *bp, |
| struct perf_sample_data *data, struct pt_regs *regs) |
| { |
| struct perf_event_attr attr; |
| |
| attr = bp->attr; |
| attr.disabled = true; |
| modify_user_hw_breakpoint(bp, &attr); |
| } |
| |
| static int set_single_step(struct task_struct *tsk, unsigned long addr) |
| { |
| struct perf_event *bp; |
| struct perf_event_attr attr; |
| struct arch_hw_breakpoint *info; |
| struct thread_struct *thread = &tsk->thread; |
| |
| bp = thread->hbp_break[0]; |
| if (!bp) { |
| ptrace_breakpoint_init(&attr); |
| |
| attr.bp_addr = addr; |
| attr.bp_len = HW_BREAKPOINT_LEN_8; |
| attr.bp_type = HW_BREAKPOINT_X; |
| |
| bp = register_user_hw_breakpoint(&attr, ptrace_triggered, |
| NULL, tsk); |
| if (IS_ERR(bp)) |
| return PTR_ERR(bp); |
| |
| thread->hbp_break[0] = bp; |
| } else { |
| int err; |
| |
| attr = bp->attr; |
| attr.bp_addr = addr; |
| |
| /* Reenable breakpoint */ |
| attr.disabled = false; |
| err = modify_user_hw_breakpoint(bp, &attr); |
| if (unlikely(err)) |
| return err; |
| |
| csr_write64(attr.bp_addr, LOONGARCH_CSR_IB0ADDR); |
| } |
| info = counter_arch_bp(bp); |
| info->mask = TASK_SIZE - 1; |
| |
| return 0; |
| } |
| |
| /* ptrace API */ |
| void user_enable_single_step(struct task_struct *task) |
| { |
| struct thread_info *ti = task_thread_info(task); |
| |
| set_single_step(task, task_pt_regs(task)->csr_era); |
| task->thread.single_step = task_pt_regs(task)->csr_era; |
| set_ti_thread_flag(ti, TIF_SINGLESTEP); |
| } |
| |
| void user_disable_single_step(struct task_struct *task) |
| { |
| clear_tsk_thread_flag(task, TIF_SINGLESTEP); |
| } |
| #endif |