* [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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox