xtensa/esp32s3: Stop an unreportable cache fault from livelocking.

Reporting a fault can itself fault.  syslog reaches memory the fault being
reported may have made unreachable, so esp32s3_pagefault_dispatch() is
re-entered from inside its own _alert() and never returns, and the console
fills with the same half-printed line forever.  Found under Espressif's QEMU,
where PSRAM never initialises and the kernel build needs it; the board's
PSRAM works, so hardware does not take this path.

A fault repeating at the same address and PC is not helped by reporting it
again, so the dispatcher tries three times and then halts with interrupts
off.  esp32s3_userfault_abort() clears the count through
esp32s3_pagefault_clear_repeat(): reaching it means the fault was contained,
so only unbroken recursion stops the machine, and three probes at one
address do not halt a healthy system.

Verified under QEMU: 12,958,521 bytes of output in 60 s before, four reports
and a halt after.  On an ESP32-S3 DevKitC, esp32s3-devkit:kernel_oct, three
identical sandbox probes in one boot are all contained.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
This commit is contained in:
Marco Casaroli 2026-07-28 23:57:28 +02:00 • committed by Alan C. Assis
parent 802ccef86f
commit df782bd1e5
3 changed files with 84 additions and 0 deletions

View file

@ -66,6 +66,18 @@
static volatile int g_pf_selftest_hits;
#endif
/* How many times to let the same fault be reported before giving up on
* reporting it. Note the report may not survive even once -- see the
* comment in the dispatcher -- so this bounds the damage rather than
* guaranteeing a legible message.
*/
#define PF_REPEAT_LIMIT 3
static uintptr_t g_pf_last_vaddr;
static uintptr_t g_pf_last_pc;
static int g_pf_repeats;
/****************************************************************************
* Public Functions
****************************************************************************/
@ -125,6 +137,36 @@ int esp32s3_pagefault_dispatch(int exccause, uint32_t *regs)
}
#endif
/* Reporting a fault can itself fault. syslog reaches memory that the
* very fault being reported may have made unreachable -- PSRAM that never
* initialised, say -- and then this handler is re-entered from inside its
* own _alert(). The console fills with the same line severed part-way
* through EXCVADDR, forever, and nothing legible ever reaches it.
*
* A fault repeating at the same address and PC is not going to be helped
* by reporting it again. Try a few times, then stop and halt. The lines
* may still be truncated -- the print is what faults, so it cannot be made
* to complete from here -- but a handful of severed lines followed by
* silence is diagnosable, and an endless stream of them is not.
*/
if (vaddr == g_pf_last_vaddr && pc == g_pf_last_pc)
{
if (++g_pf_repeats >= PF_REPEAT_LIMIT)
{
up_irq_save();
for (; ; )
{
}
}
}
else
{
g_pf_last_vaddr = vaddr;
g_pf_last_pc = pc;
g_pf_repeats = 0;
}
/* Report the precise fault (with its tracking EXCVADDR) and decline to
* service it, so the caller falls through to the panic / abort path.
*/
@ -135,3 +177,20 @@ int esp32s3_pagefault_dispatch(int exccause, uint32_t *regs)
return -EFAULT;
}
/****************************************************************************
* Name: esp32s3_pagefault_clear_repeat
*
* Description:
* Forget the last serviced fault. Called once a fault has been contained
* some other way -- the task terminated -- so that later, unrelated faults
* at the same address are not counted as runaway recursion.
*
****************************************************************************/
void esp32s3_pagefault_clear_repeat(void)
{
g_pf_last_vaddr = 0;
g_pf_last_pc = 0;
g_pf_repeats = 0;
}

View file

@ -59,4 +59,15 @@
int esp32s3_pagefault_dispatch(int exccause, uint32_t *regs);
/****************************************************************************
* Name: esp32s3_pagefault_clear_repeat
*
* Description:
* Forget the last serviced fault, so that later faults at the same address
* are not mistaken for runaway recursion.
*
****************************************************************************/
void esp32s3_pagefault_clear_repeat(void);
#endif /* __ARCH_XTENSA_SRC_ESP32S3_ESP32S3_PAGEFAULT_H */

View file

@ -41,6 +41,9 @@
#include "signal/signal.h"
#include "esp32s3_userfault.h"
#ifdef CONFIG_ESP32S3_PAGEFAULT
#include "esp32s3_pagefault.h"
#endif
/****************************************************************************
* Public Functions
@ -82,6 +85,17 @@ uint32_t *esp32s3_userfault_abort(int exccause, uint32_t *regs)
struct tcb_s *tcb = this_task();
siginfo_t info;
#ifdef CONFIG_ESP32S3_PAGEFAULT
/* Reaching here means the fault was contained and the system carried on,
* so the dispatcher's repeat counter has served its purpose. Clear it, or
* a probe run three times at one address would trip that guard and halt a
* perfectly healthy system. Only *unbroken* recursion -- a report that
* faults before the abort can happen -- should stop the machine.
*/
esp32s3_pagefault_clear_repeat();
#endif
_alert("SIGSEGV task %s: EXCCAUSE=%d EXCVADDR=%08x PC=%08x\n",
get_task_name(tcb), exccause, (unsigned)regs[REG_EXCVADDR],
(unsigned)regs[REG_PC]);