* [PATCH] perf unwind-libdw: Fix reading the stack of a 32-bit task
@ 2026-09-23 19:34 Stian Halseth
2026-09-23 19:45 ` sashiko-bot
2026-09-23 20:33 ` Ian Rogers
0 siblings, 2 replies; 5+ messages in thread
From: Stian Halseth @ 2026-09-23 19:34 UTC (permalink / raw)
To: Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, Peter Zijlstra, Ingo Molnar, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Adrian Hunter, James Clark,
linux-perf-users, sparclinux, linux-kernel, Stian Halseth
memory_read() hands libdw 8 bytes whatever the sampled task's ABI. For a
32-bit task libdw keeps the low 32 bits (__libdwfl_frame_reg_set()), so
on a big-endian machine each word read from the stack or a DSO is the
one at addr + 4, and unwinding stops at the first frame restored from
the stack. On sparc the 8-byte load from a stack address that is only
4-byte aligned even traps, and perf dies with SIGBUS. Read 4 bytes for a
PERF_SAMPLE_REGS_ABI_32 sample.
A recording of the other byte order has each sample swapped in 8-byte
units when it is read (perf_event__all64_swap()), which leaves the
4-byte words of a 32-bit stack dump reversed and paired up wrongly.
Undo the swap of the unit holding the word and swap the word itself, as
__evsel__parse_sample() does for PERF_SAMPLE_CPU.
Suggested-by: Ian Rogers <irogers@google.com>
Signed-off-by: Stian Halseth <stian@itx.no>
---
Ian raised the cross-endian case in review of the sparc64 perf_regs
series:
https://lore.kernel.org/all/CAP-5=fUuRm3dWra4a-6eNF3F=tafcHvvDipqmDEXN5+ti-sW0A@mail.gmail.com/
Tested with 32-bit sparc programs (static, dynamic and multithreaded) on
top of that series, with a local change so perf recognises sparc32plus
binaries: natively on sparc64 perf died with SIGBUS before and unwinds
complete chains after; cross-endian on x86 no call chain was complete
before and all are after. 64-bit unwinding is unchanged in both.
Words read from a DSO rather than the stack dump are still not swapped
for a recording of the other byte order, whatever the word size, as
before this change.
tools/perf/util/unwind-libdw.c | 46 ++++++++++++++++++++++++++++------
1 file changed, 39 insertions(+), 7 deletions(-)
diff --git a/tools/perf/util/unwind-libdw.c b/tools/perf/util/unwind-libdw.c
index 4ccfcc7c2dfc..d49901b1571e 100644
--- a/tools/perf/util/unwind-libdw.c
+++ b/tools/perf/util/unwind-libdw.c
@@ -4,6 +4,7 @@
#include <elfutils/libdw.h>
#include <elfutils/libdwfl.h>
#include <inttypes.h>
+#include <byteswap.h>
#include <errno.h>
#include "debug.h"
#include "dso.h"
@@ -17,6 +18,8 @@
#include <linux/types.h>
#include <linux/zalloc.h>
#include "event.h"
+#include "evsel.h"
+#include "memswap.h"
#include "perf_regs.h"
#include "callchain.h"
#include "util/env.h"
@@ -203,9 +206,10 @@ static bool get_thread(Dwfl *dwfl __maybe_unused, pid_t tid, void *arg,
}
static int access_dso_mem(struct unwind_info *ui, Dwarf_Addr addr,
- Dwarf_Word *data)
+ Dwarf_Word *data, size_t len)
{
struct addr_location al;
+ union u64_swap u;
ssize_t size;
struct dso *dso;
@@ -218,15 +222,38 @@ static int access_dso_mem(struct unwind_info *ui, Dwarf_Addr addr,
if (!dso)
goto out_fail;
- size = dso__data_read_addr(dso, al.map, ui->machine, addr, (u8 *) data, sizeof(*data));
+ size = dso__data_read_addr(dso, al.map, ui->machine, addr, (u8 *)&u, len);
addr_location__exit(&al);
- return !(size == sizeof(*data));
+ if (size != (ssize_t)len)
+ return 1;
+ *data = len == sizeof(u32) ? u.val32[0] : u.val64;
+ return 0;
out_fail:
addr_location__exit(&al);
return -1;
}
+/*
+ * libdw expects a 32-bit task's words zero-extended. A recording of the
+ * other byte order was swapped in 8-byte units when it was read (see
+ * perf_event__all64_swap()), so a 4-byte word is picked out of its unit with
+ * that swap undone, as for PERF_SAMPLE_CPU in __evsel__parse_sample().
+ */
+static Dwarf_Word stack_word(struct stack_dump *stack, int offset, size_t len,
+ bool swapped)
+{
+ union u64_swap u;
+
+ if (len == sizeof(u64))
+ return *(Dwarf_Word *)&stack->data[offset];
+ if (!swapped)
+ return *(u32 *)&stack->data[offset];
+
+ u.val64 = bswap_64(*(u64 *)&stack->data[offset & ~7]);
+ return bswap_32(u.val32[(offset & 4) / 4]);
+}
+
static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word *result,
void *arg)
{
@@ -234,11 +261,16 @@ static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word *
struct unwind_info *ui = dwfl_ui_ti->ui;
struct stack_dump *stack = &ui->sample->user_stack;
u64 start, end;
+ bool swapped;
+ size_t len;
int offset;
int ret;
if (!ui->sample->user_regs)
return false;
+ len = ui->sample->user_regs->abi == PERF_SAMPLE_REGS_ABI_32 ?
+ sizeof(u32) : sizeof(u64);
+ swapped = ui->sample->evsel && ui->sample->evsel->needs_swap;
ret = perf_reg_value(&start, ui->sample->user_regs,
perf_arch_reg_sp(ui->e_machine));
@@ -248,11 +280,11 @@ static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word *
end = start + stack->size;
/* Check overflow. */
- if (addr + sizeof(Dwarf_Word) < addr)
+ if (addr + len < addr)
return false;
- if (addr < start || addr + sizeof(Dwarf_Word) > end) {
- ret = access_dso_mem(ui, addr, result);
+ if (addr < start || addr + len > end) {
+ ret = access_dso_mem(ui, addr, result, len);
if (ret) {
pr_debug("unwind: access_mem 0x%" PRIx64 " not inside range"
" 0x%" PRIx64 "-0x%" PRIx64 "\n",
@@ -263,7 +295,7 @@ static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word *
}
offset = addr - start;
- *result = *(Dwarf_Word *)&stack->data[offset];
+ *result = stack_word(stack, offset, len, swapped);
pr_debug("unwind: access_mem addr 0x%" PRIx64 ", val %lx, offset %d\n",
addr, (unsigned long)*result, offset);
return true;
base-commit: edd8a9fe2eca009599e013a29c421c7a6b5ad1b9
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* Re: [PATCH] perf unwind-libdw: Fix reading the stack of a 32-bit task
2026-09-23 19:34 [PATCH] perf unwind-libdw: Fix reading the stack of a 32-bit task Stian Halseth
@ 2026-09-23 19:45 ` sashiko-bot
2026-09-23 20:29 ` Stian Halseth
2026-09-23 20:33 ` Ian Rogers
1 sibling, 1 reply; 5+ messages in thread
From: sashiko-bot @ 2026-09-23 19:45 UTC (permalink / raw)
To: Stian Halseth; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] perf: incorrect data extraction for unaligned stack reads during cross-endian unwinding
--
commit c969a9ae6966b9f317efd438b881a5fa16635c84
Author: Stian Halseth <stian@itx.no>
perf unwind-libdw: Fix reading the stack of a 32-bit task
This commit fixes reading the stack of a 32-bit task during unwinding on
big-endian machines by reading 4 bytes instead of 8, preventing traps. It
also corrects word ordering for cross-endian unwinding.
> diff --git a/tools/perf/util/unwind-libdw.c b/tools/perf/util/unwind-libdw.c
> index 4ccfcc7c2dfca..d49901b1571e9 100644
> --- a/tools/perf/util/unwind-libdw.c
> +++ b/tools/perf/util/unwind-libdw.c
[ ... ]
> +/*
> + * libdw expects a 32-bit task's words zero-extended. A recording of the
> + * other byte order was swapped in 8-byte units when it was read (see
> + * perf_event__all64_swap()), so a 4-byte word is picked out of its unit with
> + * that swap undone, as for PERF_SAMPLE_CPU in __evsel__parse_sample().
> + */
> +static Dwarf_Word stack_word(struct stack_dump *stack, int offset, size_t len,
> + bool swapped)
> +{
> + union u64_swap u;
> +
> + if (len == sizeof(u64))
> + return *(Dwarf_Word *)&stack->data[offset];
> + if (!swapped)
> + return *(u32 *)&stack->data[offset];
> +
> + u.val64 = bswap_64(*(u64 *)&stack->data[offset & ~7]);
[Severity: Low]
Does this bitwise logic truncate the offset to a 4-byte boundary?
If a DWARF CFI expression requests a stack read at an unaligned address
during cross-endian unwinding, the calculation discards the unaligned bits.
While register unwinding typically targets 4-byte aligned addresses on
32-bit architectures, could a malformed or custom ELF with non-standard
CFI trigger an unaligned read? If so, would this extract the wrong bytes
instead of the requested data, potentially corrupting the unwind results?
> + return bswap_32(u.val32[(offset & 4) / 4]);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260923193432.2489729-1-stian@itx.no?part=1
^ permalink raw reply [flat|nested] 5+ messages in thread* Re: [PATCH] perf unwind-libdw: Fix reading the stack of a 32-bit task
2026-09-23 19:45 ` sashiko-bot
@ 2026-09-23 20:29 ` Stian Halseth
0 siblings, 0 replies; 5+ messages in thread
From: Stian Halseth @ 2026-09-23 20:29 UTC (permalink / raw)
To: sashiko-reviews; +Cc: linux-perf-users
[-- Attachment #1: Type: text/plain, Size: 1383 bytes --]
On Wed, 2026-09-23 at 19:45 +0000, sashiko-bot@kernel.org wrote:
>
> [Severity: Low]
> Does this bitwise logic truncate the offset to a 4-byte boundary?
Yes, it does, but only for an address that isn't 4-byte aligned, and I
don't think that can happen here.
libdw only uses memory_read() to restore registers, and the save slots
are always word-aligned on a 32-bit ABI.
In theory, a malformed CFI could ask for one, though. But if that were
the case, the read would stay inside the stack dump, and the unwind
would be wrong either way. On a strict-alignment machine like sparc,
the native reads would trap on such an address as well.
I'd rather avoid adding handling for a case that shouldn't occur, butif preferred, I can make the swapped case refuse an unaligned read
instead of returning the aligned word.
>
> If a DWARF CFI expression requests a stack read at an unaligned
> address
> during cross-endian unwinding, the calculation discards the unaligned
> bits.
>
> While register unwinding typically targets 4-byte aligned addresses
> on
> 32-bit architectures, could a malformed or custom ELF with non-
> standard
> CFI trigger an unaligned read? If so, would this extract the wrong
> bytes
> instead of the requested data, potentially corrupting the unwind
> results?
>
> > + return bswap_32(u.val32[(offset & 4) / 4]);
> > +}
>
[-- Attachment #2: This is a digitally signed message part --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] perf unwind-libdw: Fix reading the stack of a 32-bit task
2026-09-23 19:34 [PATCH] perf unwind-libdw: Fix reading the stack of a 32-bit task Stian Halseth
2026-09-23 19:45 ` sashiko-bot
@ 2026-09-23 20:33 ` Ian Rogers
2026-09-24 17:08 ` Arnaldo Carvalho de Melo
1 sibling, 1 reply; 5+ messages in thread
From: Ian Rogers @ 2026-09-23 20:33 UTC (permalink / raw)
To: Stian Halseth
Cc: Arnaldo Carvalho de Melo, Namhyung Kim, Peter Zijlstra,
Ingo Molnar, Mark Rutland, Alexander Shishkin, Jiri Olsa,
Adrian Hunter, James Clark, linux-perf-users, sparclinux,
linux-kernel
On Wed, Sep 23, 2026 at 12:34 PM Stian Halseth <stian@itx.no> wrote:
>
> memory_read() hands libdw 8 bytes whatever the sampled task's ABI. For a
> 32-bit task libdw keeps the low 32 bits (__libdwfl_frame_reg_set()), so
> on a big-endian machine each word read from the stack or a DSO is the
> one at addr + 4, and unwinding stops at the first frame restored from
> the stack. On sparc the 8-byte load from a stack address that is only
> 4-byte aligned even traps, and perf dies with SIGBUS. Read 4 bytes for a
> PERF_SAMPLE_REGS_ABI_32 sample.
>
> A recording of the other byte order has each sample swapped in 8-byte
> units when it is read (perf_event__all64_swap()), which leaves the
> 4-byte words of a 32-bit stack dump reversed and paired up wrongly.
> Undo the swap of the unit holding the word and swap the word itself, as
> __evsel__parse_sample() does for PERF_SAMPLE_CPU.
>
> Suggested-by: Ian Rogers <irogers@google.com>
> Signed-off-by: Stian Halseth <stian@itx.no>
Reviewed-by: Ian Rogers <irogers@google.com>
Thanks!
Ian
> ---
> Ian raised the cross-endian case in review of the sparc64 perf_regs
> series:
> https://lore.kernel.org/all/CAP-5=fUuRm3dWra4a-6eNF3F=tafcHvvDipqmDEXN5+ti-sW0A@mail.gmail.com/
>
> Tested with 32-bit sparc programs (static, dynamic and multithreaded) on
> top of that series, with a local change so perf recognises sparc32plus
> binaries: natively on sparc64 perf died with SIGBUS before and unwinds
> complete chains after; cross-endian on x86 no call chain was complete
> before and all are after. 64-bit unwinding is unchanged in both.
>
> Words read from a DSO rather than the stack dump are still not swapped
> for a recording of the other byte order, whatever the word size, as
> before this change.
>
> tools/perf/util/unwind-libdw.c | 46 ++++++++++++++++++++++++++++------
> 1 file changed, 39 insertions(+), 7 deletions(-)
>
> diff --git a/tools/perf/util/unwind-libdw.c b/tools/perf/util/unwind-libdw.c
> index 4ccfcc7c2dfc..d49901b1571e 100644
> --- a/tools/perf/util/unwind-libdw.c
> +++ b/tools/perf/util/unwind-libdw.c
> @@ -4,6 +4,7 @@
> #include <elfutils/libdw.h>
> #include <elfutils/libdwfl.h>
> #include <inttypes.h>
> +#include <byteswap.h>
> #include <errno.h>
> #include "debug.h"
> #include "dso.h"
> @@ -17,6 +18,8 @@
> #include <linux/types.h>
> #include <linux/zalloc.h>
> #include "event.h"
> +#include "evsel.h"
> +#include "memswap.h"
> #include "perf_regs.h"
> #include "callchain.h"
> #include "util/env.h"
> @@ -203,9 +206,10 @@ static bool get_thread(Dwfl *dwfl __maybe_unused, pid_t tid, void *arg,
> }
>
> static int access_dso_mem(struct unwind_info *ui, Dwarf_Addr addr,
> - Dwarf_Word *data)
> + Dwarf_Word *data, size_t len)
> {
> struct addr_location al;
> + union u64_swap u;
> ssize_t size;
> struct dso *dso;
>
> @@ -218,15 +222,38 @@ static int access_dso_mem(struct unwind_info *ui, Dwarf_Addr addr,
> if (!dso)
> goto out_fail;
>
> - size = dso__data_read_addr(dso, al.map, ui->machine, addr, (u8 *) data, sizeof(*data));
> + size = dso__data_read_addr(dso, al.map, ui->machine, addr, (u8 *)&u, len);
>
> addr_location__exit(&al);
> - return !(size == sizeof(*data));
> + if (size != (ssize_t)len)
> + return 1;
> + *data = len == sizeof(u32) ? u.val32[0] : u.val64;
> + return 0;
> out_fail:
> addr_location__exit(&al);
> return -1;
> }
>
> +/*
> + * libdw expects a 32-bit task's words zero-extended. A recording of the
> + * other byte order was swapped in 8-byte units when it was read (see
> + * perf_event__all64_swap()), so a 4-byte word is picked out of its unit with
> + * that swap undone, as for PERF_SAMPLE_CPU in __evsel__parse_sample().
> + */
> +static Dwarf_Word stack_word(struct stack_dump *stack, int offset, size_t len,
> + bool swapped)
> +{
> + union u64_swap u;
> +
> + if (len == sizeof(u64))
> + return *(Dwarf_Word *)&stack->data[offset];
> + if (!swapped)
> + return *(u32 *)&stack->data[offset];
> +
> + u.val64 = bswap_64(*(u64 *)&stack->data[offset & ~7]);
> + return bswap_32(u.val32[(offset & 4) / 4]);
> +}
> +
> static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word *result,
> void *arg)
> {
> @@ -234,11 +261,16 @@ static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word *
> struct unwind_info *ui = dwfl_ui_ti->ui;
> struct stack_dump *stack = &ui->sample->user_stack;
> u64 start, end;
> + bool swapped;
> + size_t len;
> int offset;
> int ret;
>
> if (!ui->sample->user_regs)
> return false;
> + len = ui->sample->user_regs->abi == PERF_SAMPLE_REGS_ABI_32 ?
> + sizeof(u32) : sizeof(u64);
> + swapped = ui->sample->evsel && ui->sample->evsel->needs_swap;
>
> ret = perf_reg_value(&start, ui->sample->user_regs,
> perf_arch_reg_sp(ui->e_machine));
> @@ -248,11 +280,11 @@ static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word *
> end = start + stack->size;
>
> /* Check overflow. */
> - if (addr + sizeof(Dwarf_Word) < addr)
> + if (addr + len < addr)
> return false;
>
> - if (addr < start || addr + sizeof(Dwarf_Word) > end) {
> - ret = access_dso_mem(ui, addr, result);
> + if (addr < start || addr + len > end) {
> + ret = access_dso_mem(ui, addr, result, len);
> if (ret) {
> pr_debug("unwind: access_mem 0x%" PRIx64 " not inside range"
> " 0x%" PRIx64 "-0x%" PRIx64 "\n",
> @@ -263,7 +295,7 @@ static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word *
> }
>
> offset = addr - start;
> - *result = *(Dwarf_Word *)&stack->data[offset];
> + *result = stack_word(stack, offset, len, swapped);
> pr_debug("unwind: access_mem addr 0x%" PRIx64 ", val %lx, offset %d\n",
> addr, (unsigned long)*result, offset);
> return true;
>
> base-commit: edd8a9fe2eca009599e013a29c421c7a6b5ad1b9
> --
> 2.55.0
>
^ permalink raw reply [flat|nested] 5+ messages in thread* Re: [PATCH] perf unwind-libdw: Fix reading the stack of a 32-bit task
2026-09-23 20:33 ` Ian Rogers
@ 2026-09-24 17:08 ` Arnaldo Carvalho de Melo
0 siblings, 0 replies; 5+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-09-24 17:08 UTC (permalink / raw)
To: Ian Rogers
Cc: Stian Halseth, Namhyung Kim, Peter Zijlstra, Ingo Molnar,
Mark Rutland, Alexander Shishkin, Jiri Olsa, Adrian Hunter,
James Clark, linux-perf-users, sparclinux, linux-kernel
On Wed, Sep 23, 2026 at 01:33:48PM -0700, Ian Rogers wrote:
> On Wed, Sep 23, 2026 at 12:34 PM Stian Halseth <stian@itx.no> wrote:
> >
> > memory_read() hands libdw 8 bytes whatever the sampled task's ABI. For a
> > 32-bit task libdw keeps the low 32 bits (__libdwfl_frame_reg_set()), so
> > on a big-endian machine each word read from the stack or a DSO is the
> > one at addr + 4, and unwinding stops at the first frame restored from
> > the stack. On sparc the 8-byte load from a stack address that is only
> > 4-byte aligned even traps, and perf dies with SIGBUS. Read 4 bytes for a
> > PERF_SAMPLE_REGS_ABI_32 sample.
> >
> > A recording of the other byte order has each sample swapped in 8-byte
> > units when it is read (perf_event__all64_swap()), which leaves the
> > 4-byte words of a 32-bit stack dump reversed and paired up wrongly.
> > Undo the swap of the unit holding the word and swap the word itself, as
> > __evsel__parse_sample() does for PERF_SAMPLE_CPU.
> >
> > Suggested-by: Ian Rogers <irogers@google.com>
> > Signed-off-by: Stian Halseth <stian@itx.no>
>
> Reviewed-by: Ian Rogers <irogers@google.com>
Thanks, applied to perf-tools-next, for v7.4.
- Arnaldo
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-09-24 17:08 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-23 19:34 [PATCH] perf unwind-libdw: Fix reading the stack of a 32-bit task Stian Halseth
2026-09-23 19:45 ` sashiko-bot
2026-09-23 20:29 ` Stian Halseth
2026-09-23 20:33 ` Ian Rogers
2026-09-24 17:08 ` Arnaldo Carvalho de Melo
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.