[PATCH] arm64: perf: truncate compat regs

From: Mark Rutland
Date: Fri Apr 16 2021 - 08:58:54 EST


For compat userspace, it doesn't generally make sense for the upper 32
bits of GPRs to be set, as these bits don't really exist in AArch32.
However, for structural reasons the kernel may transiently set the upper
32 bits of registers in pt_regs at points where a perf sample can be
taken.

We tried to avoid this happening in commit:

15956689a0e60aa0 ("arm64: compat: Ensure upper 32 bits of x0 are zero on syscall return")

... by having invoke_syscall() truncate the return value for compat
tasks, with helpers in <asm/syscall.h> extending the return value when
required.

Unfortunately this is not complete, as there are other places where we
assign the return value, such as when el0_svc_common() sets up a return
of -ENOSYS.

Further, this approach breaks the audit code, which relies on the upper
32 bits of the return value.

Instead, let's have the perf code explicitly truncate the user regs to
32 bits, and otherwise preserve those within the kernel.

Fixes: 15956689a0e60aa0 ("arm64: compat: Ensure upper 32 bits of x0 are zero on syscall return")
Signed-off-by: Mark Rutland <mark.rutland@xxxxxxx>
Cc: Will Deacon <will@xxxxxxxxxx>
---
arch/arm64/include/asm/syscall.h | 11 +----------
arch/arm64/kernel/perf_regs.c | 26 ++++++++++++++++----------
arch/arm64/kernel/syscall.c | 3 ---
3 files changed, 17 insertions(+), 23 deletions(-)

diff --git a/arch/arm64/include/asm/syscall.h b/arch/arm64/include/asm/syscall.h
index cfc0672013f6..0ebeaf6dbd45 100644
--- a/arch/arm64/include/asm/syscall.h
+++ b/arch/arm64/include/asm/syscall.h
@@ -35,9 +35,6 @@ static inline long syscall_get_error(struct task_struct *task,
{
unsigned long error = regs->regs[0];

- if (is_compat_thread(task_thread_info(task)))
- error = sign_extend64(error, 31);
-
return IS_ERR_VALUE(error) ? error : 0;
}

@@ -51,13 +48,7 @@ static inline void syscall_set_return_value(struct task_struct *task,
struct pt_regs *regs,
int error, long val)
{
- if (error)
- val = error;
-
- if (is_compat_thread(task_thread_info(task)))
- val = lower_32_bits(val);
-
- regs->regs[0] = val;
+ regs->regs[0] = (long) error ? error : val;
}

#define SYSCALL_MAX_ARGS 6
diff --git a/arch/arm64/kernel/perf_regs.c b/arch/arm64/kernel/perf_regs.c
index f6f58e6265df..296f0c55b4e2 100644
--- a/arch/arm64/kernel/perf_regs.c
+++ b/arch/arm64/kernel/perf_regs.c
@@ -9,6 +9,17 @@
#include <asm/perf_regs.h>
#include <asm/ptrace.h>

+static u64 __perf_reg_value(struct pt_regs *regs, int idx)
+{
+ if ((u32)idx == PERF_REG_ARM64_SP)
+ return regs->sp;
+
+ if ((u32)idx == PERF_REG_ARM64_PC)
+ return regs->pc;
+
+ return regs->regs[idx];
+}
+
u64 perf_reg_value(struct pt_regs *regs, int idx)
{
if (WARN_ON_ONCE((u32)idx >= PERF_REG_ARM64_MAX))
@@ -38,20 +49,15 @@ u64 perf_reg_value(struct pt_regs *regs, int idx)
*/
if (compat_user_mode(regs)) {
if ((u32)idx == PERF_REG_ARM64_SP)
- return regs->compat_sp;
+ return lower_32_bits(regs->compat_sp);
if ((u32)idx == PERF_REG_ARM64_LR)
- return regs->compat_lr;
+ return lower_32_bits(regs->compat_lr);
if (idx == 15)
- return regs->pc;
+ return lower_32_bits(regs->pc);
+ return lower_32_bits(__perf_reg_value(regs, idx));
}

- if ((u32)idx == PERF_REG_ARM64_SP)
- return regs->sp;
-
- if ((u32)idx == PERF_REG_ARM64_PC)
- return regs->pc;
-
- return regs->regs[idx];
+ return __perf_reg_value(regs, idx);
}

#define REG_RESERVED (~((1ULL << PERF_REG_ARM64_MAX) - 1))
diff --git a/arch/arm64/kernel/syscall.c b/arch/arm64/kernel/syscall.c
index b9cf12b271d7..84314fae4b5c 100644
--- a/arch/arm64/kernel/syscall.c
+++ b/arch/arm64/kernel/syscall.c
@@ -51,9 +51,6 @@ static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
ret = do_ni_syscall(regs, scno);
}

- if (is_compat_task())
- ret = lower_32_bits(ret);
-
regs->regs[0] = ret;
}

--
2.11.0