mirror of
https://github.com/torvalds/linux.git
synced 2026-10-28 17:55:02 +01:00
So the syscall return sequence is as follows:
A syscall return to userspace is prepared and then a short asm sequence
that actually does the RFI. Note that this asm range is restartable i.e.
EE is still on, so an interrupt (e.g. decrementer or external interrupt)
can hit while SRR/GPRs are being loaded. This is defined via:
RESTART_TABLE(.Lsyscall_rst_start, .Lsyscall_rst_end, syscall_restart)
This restart table then sends us to syscall_restart rather than resuming
in the middle of the RFI. The same stub is also used if irq_happened
already has a pending bit (soft-masked irq that has not been replayed
yet (PowerPC special case of local_irq_disable())).
Here is a bit of a flow of sequence of code to visualize:
syscall_exit_prepare
decide full-GPR restore (_TIF_RESTOREALL) for signal,
rt_sigreturn or syscall trace
save that in regs->exit_result and return it in r3
|
v
.Lsyscall_rst_start .. _end EE still on
irq_happened set or interrupt in this range?
| no | yes
v v
cmpdi r3,0 syscall_exit_restart
restore all / zero replay irq, try exit again
volatiles; RFI must return flags in r3
again for the same cmpdi
Now r3 after prepare is the flags word, not the actual syscall return. A nested
interrupt clobbers it, so the restart stub reloads RESULT into r3 and the
C handler (syscall_exit_restart()) should put the flags back (because later asm
checks whether r3 returned from C has _TIF_RESTOREALL set or not):
cmpdi r3, 0
bne .Lsyscall_restore_regs
Note that syscall_exit_restart() already ORs any new _TIF_RESTOREALL into
exit_result, but then it only returns the new sample and not the full
regs->exit_result.
That sample could be often 0 even when restore-all is still required:
- rt_sigreturn / syscall trace set the bit in prepare's local
ret and in exit_result. They never set exit_flags, which is
what restart samples.
- a signal does set exit_flags but restart clears it. A
second pass through the stub then returns 0 while
exit_result still has the bit.
The asm as mentioned earlier then treats r3==0 as the fast path and
zeros r0/r4-r12. That means the userspace that needed the full register
set could SIGSEGVs, (which could happen often in ld64.so.2 like while
doing a parallel kernel build as reported by Venkat).
So we should instead return the accumulated exit_result, like how we do
in interrupt_exit_user_restart(). Note that prior to this commit
263e5159e0 ("powerpc: Fix exit_flags field placement in pt_regs for ptrace")
we were returning regs->exit_result from syscall_exit_restart(), but
this commit changed that behaviour.
Fixes: 263e5159e0 ("powerpc: Fix exit_flags field placement in pt_regs for ptrace")
Reported-by: Venkat Rao Bagalkote <venkat88@linux.ibm.com>
Closes: https://lore.kernel.org/all/75419f88-eab9-444b-bf97-28a9765819ad@linux.ibm.com/
Signed-off-by: Ritesh Harjani (IBM) <ritesh.list@gmail.com>
Tested-by: Amit Machhiwal <amachhiw@linux.ibm.com>
Tested-by: Shrikanth Hegde <sshegde@linux.ibm.com>
Tested-by: Venkat Rao Bagalkote <venkat88@linux.ibm.com>
Reviewed-by: Amit Machhiwal <amachhiw@linux.ibm.com>
Reviewed-by: Shrikanth Hegde <sshegde@linux.ibm.com>
Reviewed-by: Mukesh Kumar Chaurasiya (IBM) <mkchauras@gmail.com>
Signed-off-by: Madhavan Srinivasan <maddy@linux.ibm.com>
Link: https://patch.msgid.link/10c86c909f870d90b3094f76b692b44ebe9caeac.1787976185.git.ritesh.list@gmail.com
354 lines
8.9 KiB
C
354 lines
8.9 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <linux/context_tracking.h>
|
|
#include <linux/entry-common.h>
|
|
#include <linux/err.h>
|
|
#include <linux/compat.h>
|
|
#include <linux/rseq.h>
|
|
#include <linux/sched/debug.h> /* for show_regs */
|
|
|
|
#include <asm/kup.h>
|
|
#include <asm/cputime.h>
|
|
#include <asm/hw_irq.h>
|
|
#include <asm/interrupt.h>
|
|
#include <asm/kprobes.h>
|
|
#include <asm/paca.h>
|
|
#include <asm/ptrace.h>
|
|
#include <asm/reg.h>
|
|
#include <asm/signal.h>
|
|
#include <asm/switch_to.h>
|
|
#include <asm/syscall.h>
|
|
#include <asm/time.h>
|
|
#include <asm/tm.h>
|
|
#include <asm/unistd.h>
|
|
|
|
#if defined(CONFIG_PPC_ADV_DEBUG_REGS) && defined(CONFIG_PPC32)
|
|
unsigned long global_dbcr0[NR_CPUS];
|
|
#endif
|
|
|
|
#ifdef CONFIG_PPC_BOOK3S_64
|
|
DEFINE_STATIC_KEY_FALSE(interrupt_exit_not_reentrant);
|
|
static inline bool exit_must_hard_disable(void)
|
|
{
|
|
return static_branch_unlikely(&interrupt_exit_not_reentrant);
|
|
}
|
|
#else
|
|
static inline bool exit_must_hard_disable(void)
|
|
{
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* local irqs must be disabled. Returns false if the caller must re-enable
|
|
* them, check for new work, and try again.
|
|
*
|
|
* This should be called with local irqs disabled, but if they were previously
|
|
* enabled when the interrupt handler returns (indicating a process-context /
|
|
* synchronous interrupt) then irqs_enabled should be true.
|
|
*
|
|
* restartable is true then EE/RI can be left on because interrupts are handled
|
|
* with a restart sequence.
|
|
*/
|
|
static notrace __always_inline bool prep_irq_for_enabled_exit(bool restartable)
|
|
{
|
|
bool must_hard_disable = (exit_must_hard_disable() || !restartable);
|
|
|
|
/* This must be done with RI=1 because tracing may touch vmaps */
|
|
trace_hardirqs_on();
|
|
|
|
if (must_hard_disable)
|
|
__hard_EE_RI_disable();
|
|
|
|
#ifdef CONFIG_PPC64
|
|
/* This pattern matches prep_irq_for_idle */
|
|
if (unlikely(lazy_irq_pending_nocheck())) {
|
|
if (must_hard_disable) {
|
|
local_paca->irq_happened |= PACA_IRQ_HARD_DIS;
|
|
__hard_RI_enable();
|
|
}
|
|
trace_hardirqs_off();
|
|
|
|
return false;
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* This should be called after a syscall returns, with r3 the return value
|
|
* from the syscall. If this function returns non-zero, the system call
|
|
* exit assembly should additionally load all GPR registers and CTR and XER
|
|
* from the interrupt frame.
|
|
*
|
|
* The function graph tracer can not trace the return side of this function,
|
|
* because RI=0 and soft mask state is "unreconciled", so it is marked notrace.
|
|
*/
|
|
notrace unsigned long syscall_exit_prepare(unsigned long r3,
|
|
struct pt_regs *regs,
|
|
long scv)
|
|
{
|
|
unsigned long ti_flags;
|
|
unsigned long ret = 0;
|
|
bool is_not_scv = !IS_ENABLED(CONFIG_PPC_BOOK3S_64) || !scv;
|
|
|
|
kuap_assert_locked();
|
|
|
|
regs->result = r3;
|
|
|
|
/* Clear exit_flags so only flags set during this exit are visible */
|
|
current->thread_info.exit_flags = 0;
|
|
|
|
ti_flags = read_thread_flags();
|
|
if (unlikely(r3 >= (unsigned long)-MAX_ERRNO) && is_not_scv) {
|
|
if (likely(!(ti_flags & (_TIF_NOERROR | _TIF_RESTOREALL)))) {
|
|
r3 = -r3;
|
|
regs->ccr |= 0x10000000; /* Set SO bit in CR */
|
|
}
|
|
}
|
|
|
|
if (unlikely(ti_flags & _TIF_PERSYSCALL_MASK)) {
|
|
if (ti_flags & _TIF_RESTOREALL)
|
|
ret = _TIF_RESTOREALL;
|
|
else
|
|
regs->gpr[3] = r3;
|
|
clear_bits(_TIF_PERSYSCALL_MASK, ¤t_thread_info()->flags);
|
|
} else {
|
|
regs->gpr[3] = r3;
|
|
}
|
|
|
|
if (unlikely(ti_flags & _TIF_SYSCALL_DOTRACE)) {
|
|
ret |= _TIF_RESTOREALL;
|
|
}
|
|
|
|
syscall_exit_to_user_mode(regs);
|
|
|
|
again:
|
|
user_enter_irqoff();
|
|
if (!prep_irq_for_enabled_exit(true)) {
|
|
user_exit_irqoff();
|
|
local_irq_enable();
|
|
local_irq_disable();
|
|
goto again;
|
|
}
|
|
|
|
/* Restore user access locks last */
|
|
kuap_user_restore(regs);
|
|
ret |= current->thread_info.exit_flags;
|
|
#ifdef CONFIG_PPC64
|
|
regs->exit_result = ret;
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
#ifdef CONFIG_PPC64
|
|
notrace unsigned long syscall_exit_restart(unsigned long r3, struct pt_regs *regs)
|
|
{
|
|
unsigned long ret;
|
|
|
|
/*
|
|
* This is called when detecting a soft-pending interrupt as well as
|
|
* an alternate-return interrupt. So we can't just have the alternate
|
|
* return path clear SRR1[MSR] and set PACA_IRQ_HARD_DIS (unless
|
|
* the soft-pending case were to fix things up as well). RI might be
|
|
* disabled, in which case it gets re-enabled by __hard_irq_disable().
|
|
*/
|
|
__hard_irq_disable();
|
|
local_paca->irq_happened |= PACA_IRQ_HARD_DIS;
|
|
|
|
#ifdef CONFIG_PPC_BOOK3S_64
|
|
set_kuap(AMR_KUAP_BLOCKED);
|
|
#endif
|
|
|
|
again:
|
|
user_enter_irqoff();
|
|
if (!prep_irq_for_enabled_exit(true)) {
|
|
user_exit_irqoff();
|
|
local_irq_enable();
|
|
local_irq_disable();
|
|
goto again;
|
|
}
|
|
|
|
kuap_user_restore(regs);
|
|
ret = current_thread_info()->exit_flags & _TIF_RESTOREALL;
|
|
current_thread_info()->exit_flags &= ~_TIF_RESTOREALL;
|
|
regs->exit_result |= ret;
|
|
|
|
return regs->exit_result;
|
|
}
|
|
#endif
|
|
|
|
notrace unsigned long interrupt_exit_user_prepare(struct pt_regs *regs)
|
|
{
|
|
unsigned long ret;
|
|
|
|
BUG_ON(regs_is_unrecoverable(regs));
|
|
BUG_ON(regs_irqs_disabled(regs));
|
|
|
|
/*
|
|
* We don't need to restore AMR on the way back to userspace for KUAP.
|
|
* AMR can only have been unlocked if we interrupted the kernel.
|
|
*/
|
|
kuap_assert_locked();
|
|
|
|
/* Clear exit_flags so only flags set during this exit are visible */
|
|
current_thread_info()->exit_flags = 0;
|
|
|
|
local_irq_disable();
|
|
again:
|
|
check_return_regs_valid(regs);
|
|
user_enter_irqoff();
|
|
if (!prep_irq_for_enabled_exit(true)) {
|
|
user_exit_irqoff();
|
|
local_irq_enable();
|
|
local_irq_disable();
|
|
goto again;
|
|
}
|
|
|
|
/* Restore user access locks last */
|
|
kuap_user_restore(regs);
|
|
ret = current_thread_info()->exit_flags & _TIF_RESTOREALL;
|
|
#ifdef CONFIG_PPC64
|
|
regs->exit_result = ret;
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
void preempt_schedule_irq(void);
|
|
|
|
notrace unsigned long interrupt_exit_kernel_prepare(struct pt_regs *regs)
|
|
{
|
|
unsigned long ret = 0;
|
|
unsigned long kuap;
|
|
bool stack_store = read_thread_flags() & _TIF_EMULATE_STACK_STORE;
|
|
|
|
if (regs_is_unrecoverable(regs))
|
|
unrecoverable_exception(regs);
|
|
/*
|
|
* CT_WARN_ON comes here via program_check_exception, so avoid
|
|
* recursion.
|
|
*
|
|
* Skip the assertion on PMIs on 64e to work around a problem caused
|
|
* by NMI PMIs incorrectly taking this interrupt return path, it's
|
|
* possible for this to hit after interrupt exit to user switches
|
|
* context to user. See also the comment in the performance monitor
|
|
* handler in exceptions-64e.S
|
|
*/
|
|
if (!IS_ENABLED(CONFIG_PPC_BOOK3E_64) &&
|
|
TRAP(regs) != INTERRUPT_PROGRAM &&
|
|
TRAP(regs) != INTERRUPT_PERFMON)
|
|
CT_WARN_ON(ct_state() == CT_STATE_USER);
|
|
|
|
kuap = kuap_get_and_assert_locked();
|
|
|
|
local_irq_disable();
|
|
|
|
if (!regs_irqs_disabled(regs)) {
|
|
/* Returning to a kernel context with local irqs enabled. */
|
|
WARN_ON_ONCE(!(regs->msr & MSR_EE));
|
|
again:
|
|
|
|
check_return_regs_valid(regs);
|
|
|
|
/*
|
|
* Stack store exit can't be restarted because the interrupt
|
|
* stack frame might have been clobbered.
|
|
*/
|
|
if (!prep_irq_for_enabled_exit(unlikely(stack_store))) {
|
|
/*
|
|
* Replay pending soft-masked interrupts now. Don't
|
|
* just local_irq_enabe(); local_irq_disable(); because
|
|
* if we are returning from an asynchronous interrupt
|
|
* here, another one might hit after irqs are enabled,
|
|
* and it would exit via this same path allowing
|
|
* another to fire, and so on unbounded.
|
|
*/
|
|
hard_irq_disable();
|
|
replay_soft_interrupts();
|
|
/* Took an interrupt, may have more exit work to do. */
|
|
goto again;
|
|
}
|
|
#ifdef CONFIG_PPC64
|
|
/*
|
|
* An interrupt may clear MSR[EE] and set this concurrently,
|
|
* but it will be marked pending and the exit will be retried.
|
|
* This leaves a racy window where MSR[EE]=0 and HARD_DIS is
|
|
* clear, until interrupt_exit_kernel_restart() calls
|
|
* hard_irq_disable(), which will set HARD_DIS again.
|
|
*/
|
|
local_paca->irq_happened &= ~PACA_IRQ_HARD_DIS;
|
|
|
|
} else {
|
|
check_return_regs_valid(regs);
|
|
|
|
if (unlikely(stack_store))
|
|
__hard_EE_RI_disable();
|
|
#else
|
|
} else {
|
|
__hard_EE_RI_disable();
|
|
#endif /* CONFIG_PPC64 */
|
|
}
|
|
|
|
if (unlikely(stack_store)) {
|
|
clear_bits(_TIF_EMULATE_STACK_STORE, ¤t_thread_info()->flags);
|
|
ret = 1;
|
|
}
|
|
|
|
#ifdef CONFIG_PPC_TRANSACTIONAL_MEM
|
|
local_paca->tm_scratch = regs->msr;
|
|
#endif
|
|
|
|
/*
|
|
* 64s does not want to mfspr(SPRN_AMR) here, because this comes after
|
|
* mtmsr, which would cause Read-After-Write stalls. Hence, take the
|
|
* AMR value from the check above.
|
|
*/
|
|
kuap_kernel_restore(regs, kuap);
|
|
|
|
return ret;
|
|
}
|
|
|
|
#ifdef CONFIG_PPC64
|
|
notrace unsigned long interrupt_exit_user_restart(struct pt_regs *regs)
|
|
{
|
|
__hard_irq_disable();
|
|
local_paca->irq_happened |= PACA_IRQ_HARD_DIS;
|
|
|
|
#ifdef CONFIG_PPC_BOOK3S_64
|
|
set_kuap(AMR_KUAP_BLOCKED);
|
|
#endif
|
|
|
|
trace_hardirqs_off();
|
|
account_cpu_user_entry();
|
|
|
|
BUG_ON(!user_mode(regs));
|
|
|
|
regs->exit_result |= interrupt_exit_user_prepare(regs);
|
|
|
|
return regs->exit_result;
|
|
}
|
|
|
|
/*
|
|
* No real need to return a value here because the stack store case does not
|
|
* get restarted.
|
|
*/
|
|
notrace unsigned long interrupt_exit_kernel_restart(struct pt_regs *regs)
|
|
{
|
|
__hard_irq_disable();
|
|
local_paca->irq_happened |= PACA_IRQ_HARD_DIS;
|
|
|
|
#ifdef CONFIG_PPC_BOOK3S_64
|
|
set_kuap(AMR_KUAP_BLOCKED);
|
|
#endif
|
|
|
|
if (regs->softe == IRQS_ENABLED)
|
|
trace_hardirqs_off();
|
|
|
|
BUG_ON(user_mode(regs));
|
|
|
|
return interrupt_exit_kernel_prepare(regs);
|
|
}
|
|
#endif
|