Skip to contentSkip to conversation

unwind, perf: sframe user space unwinding

PATCH In review Josh Poimboeuf 8 participants 161 messages Jan 22, 2025 – Apr 24, 2025

Subsystems (18)
Memory management x86 VFS BPF +14 more subsystems
lore
Share & tools
lore

Josh Poimboeuf

author maintainer
#0 lore raw

v4 cover unwind, perf: sframe user space unwinding

This took a bit longer than expected. I fell into some rabbit holes chasing a number of subtle bugs. I ended up rewriting the deferral code several times. But I think the end result is much better.

The deferral request has a new interface, which helps make the implementation MUCH simpler and less fragile. As a bonus it's now possible for the request implementation to be NMI-safe.

The interface is similar to {task,irq}_work. The caller owns an unwind_work struct:

  struct unwind_work {
	struct callback_head		work;
	unwind_callback_t		func;
	int				pending;

};

For perf, struct unwind_work is embedded in struct perf_event. For ftrace maybe it would live in task_struct?

The unwind_work can be passed to the following functions:

  void unwind_deferred_init(struct unwind_work *work, unwind_callback_t func);
  int unwind_deferred_request(struct unwind_work *work, u64 *cookie);
  bool unwind_deferred_cancel(struct task_struct *task, struct unwind_work *work);

If unwind_deferred_request() returns success, the callback is guaranteed. If the callback is already pending, it returns an error, but the returned *cookie is still valid if it's nonzero.

Questions:

- Peter, I'm not sure how well this works with Intel PEBS? This just uses the original task regs, is that a problem?

- Namhyung, I rebased your perf tool patches on the new missing feature validation code, do the patches still look sane?

For testing with user space, here are the latest binutils fixes:

1785837a2570 ("ld: fix PR/32297")
938fb512184d ("ld: fix wrong SFrame info for lazy IBT PLT") 47c88752f9ad ("ld: generate SFrame stack trace info for .plt.got")

An out-of-tree glibc patch is also needed -- will attach in a reply.

Code also available at

git://git.kernel.org/pub/scm/linux/kernel/git/jpoimboe/linux.git sframe-v4

v4:
- split up patches better [Andrii]
- add callback guarantee [Andrii]
- support multiple non-contiguous elf text segments [Andrii]
- sframe section validation [Andrii]
- x86 compat mode support [Peter]
- implement guard(mmap_read_lock) [Peter]
- synchronize callback with perf event lifetime [Peter]
- detect toolchain sframe support with CONFIG_SFRAME_AS [Jens]
- get vdso working (with updated glibc patches) [Jens]
- rebase perf tool on new missing feature validation code
- brand new deferred interface and implementation
- make unwind_deferred_request() NMI-safe
- sframe debugging infrastructure
- fix some task_work bugs
- enclose multiple user copies in single STAC/CLAC pair for performance
- much banging head on wall, refactoring, simplification
- fix a lot of bugs

Previous revisions
------------------

v3:
https://lore.kernel.org/[email protected]
- move the "deferred" logic out of perf and into unwind_user with new unwind_user_deferred() interface [Steven, Mathieu]
- add more sframe sanity checks [Steven]
- make frame pointers optional depending on arch [Jens]
- fix perf event output [Namhyung]
- include Namhyung's perf tool patches
- enable sframe generation in VDSO
- fix build errors [robot]

v2:
https://lore.kernel.org/[email protected]
- rebase on v6.11-rc7
- reorganize the patches to add sframe first
- change to sframe v2
- add new perf event type: PERF_RECORD_CALLCHAIN_DEFERRED
- add new perf attribute: defer_callchain

v1:
https://lore.kernel.org/[email protected]

Original description
--------------------

Some distros have started compiling frame pointers into all their packages to enable the kernel to do system-wide profiling of user space. Unfortunately that creates a runtime performance penalty across the entire system. Using DWARF (or .eh_frame) instead isn't feasible because of complexity and slowness.

For in-kernel unwinding we solved this problem with the creation of the ORC unwinder for x86_64. Similarly, for user space the GNU assembler has created the SFrame ("Simple Frame") v2 format starting with binutils 2.41.

These patches add support for unwinding user space from the kernel using SFrame with perf. It should be easy to add user unwinding support for other components like ftrace.

There were two main challenges:

1) Finding .sframe sections in shared/dlopened libraries

The kernel has no visibility to the contents of shared libraries. This was solved by adding a PR_ADD_SFRAME option to prctl() which allows the runtime linker to manually provide the in-memory address of an .sframe section to the kernel.

2) Dealing with page faults

Keeping all binaries' sframe data pinned would likely waste a lot of memory. Instead, read it from user space on demand. That can't be done from perf NMI context due to page faults, so defer the unwind to the next user exit. Since the NMI handler doesn't do exit work, self-IPI and then schedule task work to be run on exit from the IPI.

Special thanks to Indu for the original concept, and to Steven and Peter for helping a lot with the design. And to Steven for letting me do it ;-)

Josh Poimboeuf (35):
task_work: Fix TWA_NMI_CURRENT error handling
task_work: Fix TWA_NMI_CURRENT race with __schedule()
mm: Add guard for mmap_read_lock
x86/vdso: Fix DWARF generation for getrandom()
x86/asm: Avoid emitting DWARF CFI for non-VDSO
x86/asm: Fix VDSO DWARF generation with kernel IBT enabled x86/vdso: Use SYM_FUNC_{START,END} in __kernel_vsyscall() x86/vdso: Use CFI macros in __vdso_sgx_enter_enclave() x86/vdso: Enable sframe generation in VDSO
x86/uaccess: Add unsafe_copy_from_user() implementation
unwind_user: Add user space unwinding API
unwind_user: Add frame pointer support
unwind_user/x86: Enable frame pointer unwinding on x86 perf/x86: Rename get_segment_base() and make it global
unwind_user: Add compat mode frame pointer support unwind_user/x86: Enable compat mode frame pointer unwinding on x86 unwind_user/sframe: Add support for reading .sframe headers unwind_user/sframe: Store sframe section data in per-mm maple tree unwind_user/sframe: Add support for reading .sframe contents unwind_user/sframe: Detect .sframe sections in executables unwind_user/sframe: Add prctl() interface for registering .sframe
sections
unwind_user/sframe: Wire up unwind_user to sframe unwind_user/sframe/x86: Enable sframe unwinding on x86 unwind_user/sframe: Remove .sframe section on detected corruption unwind_user/sframe: Show file name in debug output unwind_user/sframe: Enable debugging in uaccess regions unwind_user/sframe: Add .sframe validation option unwind_user/deferred: Add deferred unwinding interface unwind_user/deferred: Add unwind cache unwind_user/deferred: Make unwind deferral requests NMI-safe
perf: Remove get_perf_callchain() 'init_nr' argument
perf: Remove get_perf_callchain() 'crosstask' argument
perf: Simplify get_perf_callchain() user logic
perf: Skip user unwind if !current->mm
perf: Support deferred user callchains

Namhyung Kim (4):
perf tools: Minimal CALLCHAIN_DEFERRED support perf record: Enable defer_callchain for user callchains perf script: Display PERF_RECORD_CALLCHAIN_DEFERRED perf tools: Merge deferred user callchains

 arch/Kconfig                              |  40 ++
 arch/x86/Kconfig                          |   3 +
 arch/x86/entry/vdso/Makefile              |  10 +-
 arch/x86/entry/vdso/vdso-layout.lds.S     |   5 +-
 arch/x86/entry/vdso/vdso32/system_call.S  |  10 +-
 arch/x86/entry/vdso/vgetrandom-chacha.S   |   3 +-
 arch/x86/entry/vdso/vsgx.S                |  19 +-
 arch/x86/events/core.c                    |  10 +-
 arch/x86/include/asm/dwarf2.h             |  54 +-
 arch/x86/include/asm/linkage.h            |  29 +-
 arch/x86/include/asm/mmu.h                |   2 +-
 arch/x86/include/asm/perf_event.h         |   2 +
 arch/x86/include/asm/uaccess.h            |  39 +-
 arch/x86/include/asm/unwind_user.h        |  61 +++
 arch/x86/include/asm/unwind_user_types.h  |  17 +
 arch/x86/include/asm/vdso.h               |   1 -
 fs/binfmt_elf.c                           |  49 +-
 include/asm-generic/Kbuild                |   2 +
 include/asm-generic/unwind_user.h         |  24 +
 include/asm-generic/unwind_user_types.h   |   9 +
 include/linux/entry-common.h              |   3 +
 include/linux/mm_types.h                  |   3 +
 include/linux/mmap_lock.h                 |   2 +
 include/linux/perf_event.h                |  15 +-
 include/linux/sched.h                     |   5 +
 include/linux/sframe.h                    |  56 ++
 include/linux/unwind_deferred.h           |  52 ++
 include/linux/unwind_deferred_types.h     |  17 +
 include/linux/unwind_user.h               |  15 +
 include/linux/unwind_user_types.h         |  36 ++
 include/uapi/linux/elf.h                  |   1 +
 include/uapi/linux/perf_event.h           |  19 +-
 include/uapi/linux/prctl.h                |   5 +-
 kernel/Makefile                           |   1 +
 kernel/bpf/stackmap.c                     |  14 +-
 kernel/events/callchain.c                 |  47 +-
 kernel/events/core.c                      | 112 +++-
 kernel/fork.c                             |  14 +
 kernel/sys.c                              |   9 +
 kernel/task_work.c                        |  67 ++-
 kernel/unwind/Makefile                    |   2 +
 kernel/unwind/deferred.c                  | 266 ++++++++++
 kernel/unwind/sframe.c                    | 595 ++++++++++++++++++++++
 kernel/unwind/sframe.h                    |  71 +++
 kernel/unwind/sframe_debug.h              |  95 ++++
 kernel/unwind/user.c                      | 146 ++++++
 mm/init-mm.c                              |   2 +
 tools/include/uapi/linux/perf_event.h     |  19 +-
 tools/lib/perf/include/perf/event.h       |   7 +
 tools/perf/Documentation/perf-script.txt  |   5 +
 tools/perf/builtin-script.c               |  92 ++++
 tools/perf/util/callchain.c               |  24 +
 tools/perf/util/callchain.h               |   3 +
 tools/perf/util/event.c                   |   1 +
 tools/perf/util/evlist.c                  |   1 +
 tools/perf/util/evlist.h                  |   1 +
 tools/perf/util/evsel.c                   |  39 ++
 tools/perf/util/evsel.h                   |   1 +
 tools/perf/util/machine.c                 |   1 +
 tools/perf/util/perf_event_attr_fprintf.c |   1 +
 tools/perf/util/sample.h                  |   3 +-
 tools/perf/util/session.c                 |  78 +++
 tools/perf/util/tool.c                    |   2 +
 tools/perf/util/tool.h                    |   4 +-
 64 files changed, 2208 insertions(+), 133 deletions(-)
 create mode 100644 arch/x86/include/asm/unwind_user.h
 create mode 100644 arch/x86/include/asm/unwind_user_types.h
 create mode 100644 include/asm-generic/unwind_user.h
 create mode 100644 include/asm-generic/unwind_user_types.h
 create mode 100644 include/linux/sframe.h
 create mode 100644 include/linux/unwind_deferred.h
 create mode 100644 include/linux/unwind_deferred_types.h
 create mode 100644 include/linux/unwind_user.h
 create mode 100644 include/linux/unwind_user_types.h
 create mode 100644 kernel/unwind/Makefile
 create mode 100644 kernel/unwind/deferred.c
 create mode 100644 kernel/unwind/sframe.c
 create mode 100644 kernel/unwind/sframe.h
 create mode 100644 kernel/unwind/sframe_debug.h
 create mode 100644 kernel/unwind/user.c
Signature · 1 line
2.48.1
author maintainer
#1 lore raw

PATCH v4 1/39 task_work: Fix TWA_NMI_CURRENT error handling

It's possible for irq_work_queue() to fail if the work has already been claimed. That can happen if a TWA_NMI_CURRENT task work is requested before a previous TWA_NMI_CURRENT IRQ work on the same CPU has gotten a chance to run.

The error has to be checked before the write to task->task_works. Also the try_cmpxchg() loop isn't needed in NMI context. The TWA_NMI_CURRENT case really is special, keep things simple by keeping its code all together in one place.

Fixes 466e4d801cd4 ("task_work: Add TWA_NMI_CURRENT as an additional notify mode.")
---
 kernel/task_work.c | 39 ++++++++++++++++++++++++++-------------
 1 file changed, 26 insertions(+), 13 deletions(-)
kernel/task_work.c +26 -13
index c969f1f26be5..92024a8bfe12 100644
@@ -58,25 +58,38 @@ int task_work_add(struct task_struct *task, struct callback_head *work,
 	int flags = notify & TWA_FLAGS;
 
 	notify &= ~TWA_FLAGS;
+
 	if (notify == TWA_NMI_CURRENT) {
-		if (WARN_ON_ONCE(task != current))
+		if (WARN_ON_ONCE(!in_nmi() || task != current))
 			return -EINVAL;
 		if (!IS_ENABLED(CONFIG_IRQ_WORK))
 			return -EINVAL;
-	} else {
-		/*
-		 * Record the work call stack in order to print it in KASAN
-		 * reports.
-		 *
-		 * Note that stack allocation can fail if TWAF_NO_ALLOC flag
-		 * is set and new page is needed to expand the stack buffer.
-		 */
-		if (flags & TWAF_NO_ALLOC)
-			kasan_record_aux_stack_noalloc(work);
-		else
-			kasan_record_aux_stack(work);
+#ifdef CONFIG_IRQ_WORK
+		head = task->task_works;
+		if (unlikely(head == &work_exited))
+			return -ESRCH;
+
+		if (!irq_work_queue(this_cpu_ptr(&irq_work_NMI_resume)))
+			return -EBUSY;
+
+		work->next = head;
+		task->task_works = work;
+#endif
+		return 0;
 	}
 
+	/*
+	 * Record the work call stack in order to print it in KASAN
+	 * reports.
+	 *
+	 * Note that stack allocation can fail if TWAF_NO_ALLOC flag
+	 * is set and new page is needed to expand the stack buffer.
+	 */
+	if (flags & TWAF_NO_ALLOC)
+		kasan_record_aux_stack_noalloc(work);
+	else
+		kasan_record_aux_stack(work);
+
 	head = READ_ONCE(task->task_works);
 	do {
 		if (unlikely(head == &work_exited))
Signature · 1 line
2.48.1
maintainer
#2 lore raw

Re: [PATCH v4 01/39] task_work: Fix TWA_NMI_CURRENT error handling

Quoted 4 lines · Josh Poimboeuf

It's possible for irq_work_queue() to fail if the work has already been claimed. That can happen if a TWA_NMI_CURRENT task work is requested before a previous TWA_NMI_CURRENT IRQ work on the same CPU has gotten a chance to run.

I'm confused, if it fails then it's already pending, and we'll get the notification already. You can still add the work.

Quoted 4 lines

The error has to be checked before the write to task->task_works. Also the try_cmpxchg() loop isn't needed in NMI context. The TWA_NMI_CURRENT case really is special, keep things simple by keeping its code all together in one place.

NMIs can nest, consider #DB (which is NMI like) doing task_work_add() and getting interrupted with NMI doing the same.

Might all this be fallout from trying to fix that schedule() bug from the next patch, because as is, I don't see it.

author maintainer
#3 lore raw

Re: [PATCH v4 01/39] task_work: Fix TWA_NMI_CURRENT error handling

Quoted 8 lines · Peter Zijlstra
Quoted 4 lines · Josh Poimboeuf

It's possible for irq_work_queue() to fail if the work has already been claimed. That can happen if a TWA_NMI_CURRENT task work is requested before a previous TWA_NMI_CURRENT IRQ work on the same CPU has gotten a chance to run.

I'm confused, if it fails then it's already pending, and we'll get the notification already. You can still add the work.

Yeah, I suppose that makes sense. If the pending irq_work is already going to set TIF_NOTIFY_RESUME anyway, there's no need to do that again.

Quoted 6 lines
Quoted 4 lines

The error has to be checked before the write to task->task_works. Also the try_cmpxchg() loop isn't needed in NMI context. The TWA_NMI_CURRENT case really is special, keep things simple by keeping its code all together in one place.

NMIs can nest,

Just for my understanding: for nested NMIs, the entry code basically queues up the next NMI, so the C handler (exc_nmi) can't nest. Right?

Quoted 1 line

consider #DB (which is NMI like)

What exactly do you mean by "NMI like"? Is it because a #DB might be basically running in NMI context, if the NMI hit a breakpoint?

Quoted 1 line

doing task_work_add() and getting interrupted with NMI doing the same.

How exactly would that work? At least with my patch the #DB wouldn't be able to use TWA_NMI_CURRENT unless in_nmi() were true due to NMI hitting a breakpoint. In which case a nested NMI wouldn't actually nest, it would get "queued" by the entry code.

But yeah, I do see how the reverse can be true: somebody sets a breakpoint in task_work, right where it's fiddling with the list head. NMI calls task_work_add(TWA_NMI_CURRENT), triggering the #DB, which also calls task_work_add().

Signature · 1 line
Josh
maintainer
#4 lore raw

Re: [PATCH v4 01/39] task_work: Fix TWA_NMI_CURRENT error handling

Quoted 20 lines · Josh Poimboeuf
Quoted 8 lines · Peter Zijlstra
Quoted 4 lines · Josh Poimboeuf

It's possible for irq_work_queue() to fail if the work has already been claimed. That can happen if a TWA_NMI_CURRENT task work is requested before a previous TWA_NMI_CURRENT IRQ work on the same CPU has gotten a chance to run.

I'm confused, if it fails then it's already pending, and we'll get the notification already. You can still add the work.

Yeah, I suppose that makes sense. If the pending irq_work is already going to set TIF_NOTIFY_RESUME anyway, there's no need to do that again.

Quoted 6 lines
Quoted 4 lines

The error has to be checked before the write to task->task_works. Also the try_cmpxchg() loop isn't needed in NMI context. The TWA_NMI_CURRENT case really is special, keep things simple by keeping its code all together in one place.

NMIs can nest,

Quoted 7 lines

Just for my understanding: for nested NMIs, the entry code basically queues up the next NMI, so the C handler (exc_nmi) can't nest. Right?

Quoted 1 line

consider #DB (which is NMI like)

What exactly do you mean by "NMI like"? Is it because a #DB might be basically running in NMI context, if the NMI hit a breakpoint?

No, #DB, #BP and such are considered NMI (and will have in_nmi() true) because they can trigger anywhere, including sections where IRQs are disabled.

Quoted 4 lines
Quoted 1 line

doing task_work_add() and getting interrupted with NMI doing the same.

How exactly would that work? At least with my patch the #DB wouldn't be able to use TWA_NMI_CURRENT unless in_nmi() were true

It is, see exc_debug_kernel() doing irqentry_nmi_enter().

author maintainer
#5 lore raw

Re: [PATCH v4 01/39] task_work: Fix TWA_NMI_CURRENT error handling

Quoted 7 lines · Peter Zijlstra
Quoted 2 lines · Josh Poimboeuf

What exactly do you mean by "NMI like"? Is it because a #DB might be basically running in NMI context, if the NMI hit a breakpoint?

No, #DB, #BP and such are considered NMI (and will have in_nmi() true) because they can trigger anywhere, including sections where IRQs are disabled.

So:

- while exceptions are technically not NMI, they're "NMI" because they can occur in NMI or IRQ-disabled regions

- such "NMI" exceptions can be preempted by NMIs and "NMIs"

- NMIs can be preempted by "NMIs" but not NMIs (except in entry code!)

... did I get all that right? Not subtle at all!

I feel like in_nmi() needs a comment explaining all that nonobviousness.

Signature · 1 line
Josh
maintainer
#6 lore raw

Re: [PATCH v4 01/39] task_work: Fix TWA_NMI_CURRENT error handling

Quoted 11 lines · Josh Poimboeuf
Quoted 7 lines · Peter Zijlstra
Quoted 2 lines · Josh Poimboeuf

What exactly do you mean by "NMI like"? Is it because a #DB might be basically running in NMI context, if the NMI hit a breakpoint?

No, #DB, #BP and such are considered NMI (and will have in_nmi() true) because they can trigger anywhere, including sections where IRQs are disabled.

So:

Quoted 8 lines

- while exceptions are technically not NMI, they're "NMI" because they can occur in NMI or IRQ-disabled regions

- such "NMI" exceptions can be preempted by NMIs and "NMIs"

- NMIs can be preempted by "NMIs" but not NMIs (except in entry code!)

... did I get all that right? Not subtle at all!

Yeah, sounds about right :-)

maintainer
#7 lore raw

Re: [PATCH v4 01/39] task_work: Fix TWA_NMI_CURRENT error handling

In reply to Peter Zijlstra

/me is resurrecting the sframe work

Quoted 8 lines · On Wed, 22 Jan 2025 13:28:21 +0100 Peter Zijlstra <[email protected]> wrote:
Quoted 4 lines

The error has to be checked before the write to task->task_works. Also the try_cmpxchg() loop isn't needed in NMI context. The TWA_NMI_CURRENT case really is special, keep things simple by keeping its code all together in one place.

NMIs can nest, consider #DB (which is NMI like) doing task_work_add() and getting interrupted with NMI doing the same.

I was looking at this patch and was thinking that the trycmpxchg() is still for the race against other CPUs.

Another CPU can add a task_work for this task, right?

If so, then even though the NMI adding the task work can't be interrupted (considering there's no #DB and such), it still can clobber an update done for this tasks task_work from another CPU.

Is this patch still even needed?

-- Steve

author maintainer
#8 lore raw

PATCH v4 2/39 task_work: Fix TWA_NMI_CURRENT race with __schedule()

In reply to Josh Poimboeuf

If TWA_NMI_CURRENT task work is queued from an NMI triggered while running in __schedule() with IRQs disabled, task_work_set_notify_irq() ends up inadvertently running on the next scheduled task. So the original task doesn't get its TIF_NOTIFY_RESUME flag set and the task work may get delayed indefinitely, or may not get to run at all.

    __schedule()
	// disable irqs
	    <NMI>
		task_work_add(current, work, TWA_NMI_CURRENT);
	    </NMI>
	// current = next;
	// enable irqs
	    <IRQ>
		task_work_set_notify_irq()
		test_and_set_tsk_thread_flag(current, TIF_NOTIFY_RESUME); // wrong task!
	    </IRQ>
	// original task skips task work on its next return to user (or exit!)

Fix it by storing the task pointer along with the irq_work struct and passing that task to set_notify_resume().

Fixes 466e4d801cd4 ("task_work: Add TWA_NMI_CURRENT as an additional notify mode.")
---
 kernel/task_work.c | 30 +++++++++++++++++++++---------
 1 file changed, 21 insertions(+), 9 deletions(-)
kernel/task_work.c +21 -9
index 92024a8bfe12..f17447f69843 100644
@@ -7,12 +7,23 @@
 static struct callback_head work_exited; /* all we need is ->next == NULL */
 
 #ifdef CONFIG_IRQ_WORK
+
+struct nmi_irq_work {
+	struct irq_work work;
+	struct task_struct *task;
+};
+
 static void task_work_set_notify_irq(struct irq_work *entry)
 {
-	test_and_set_tsk_thread_flag(current, TIF_NOTIFY_RESUME);
+	struct nmi_irq_work *work = container_of(entry, struct nmi_irq_work, work);
+
+	set_notify_resume(work->task);
 }
-static DEFINE_PER_CPU(struct irq_work, irq_work_NMI_resume) =
-	IRQ_WORK_INIT_HARD(task_work_set_notify_irq);
+
+static DEFINE_PER_CPU(struct nmi_irq_work, nmi_irq_work) = {
+	.work = IRQ_WORK_INIT_HARD(task_work_set_notify_irq),
+};
+
 #endif
 
 /**
@@ -65,15 +76,21 @@ int task_work_add(struct task_struct *task, struct callback_head *work,
 		if (!IS_ENABLED(CONFIG_IRQ_WORK))
 			return -EINVAL;
 #ifdef CONFIG_IRQ_WORK
+{
+		struct nmi_irq_work *irq_work = this_cpu_ptr(&nmi_irq_work);
+
 		head = task->task_works;
 		if (unlikely(head == &work_exited))
 			return -ESRCH;
 
-		if (!irq_work_queue(this_cpu_ptr(&irq_work_NMI_resume)))
+		if (!irq_work_queue(&irq_work->work))
 			return -EBUSY;
 
+		irq_work->task = current;
+
 		work->next = head;
 		task->task_works = work;
+}
 #endif
 		return 0;
 	}
@@ -109,11 +126,6 @@ int task_work_add(struct task_struct *task, struct callback_head *work,
 	case TWA_SIGNAL_NO_IPI:
 		__set_notify_signal(task);
 		break;
-#ifdef CONFIG_IRQ_WORK
-	case TWA_NMI_CURRENT:
-		irq_work_queue(this_cpu_ptr(&irq_work_NMI_resume));
-		break;
-#endif
 	default:
 		WARN_ON_ONCE(1);
 		break;
Signature · 1 line
2.48.1
maintainer
#9 lore raw

Re: [PATCH v4 02/39] task_work: Fix TWA_NMI_CURRENT race with __schedule()

kernel/task_work.c int task_work_add(struct task_struct *task, struct callback_head *work, · line 131

Quoted 12 lines · Josh Poimboeuf
kernel/task_work.c +0 -5
@@ -109,11 +126,6 @@ int task_work_add(struct task_struct *task, struct callback_head *work,
 	case TWA_SIGNAL_NO_IPI:
 		__set_notify_signal(task);
 		break;
-#ifdef CONFIG_IRQ_WORK
-	case TWA_NMI_CURRENT:
-		irq_work_queue(this_cpu_ptr(&irq_work_NMI_resume));
-		break;
-#endif
 	default:
 		WARN_ON_ONCE(1);
 		break;

Shouldn't this go to in the previous patch?

maintainer
#10 lore raw

Re: [PATCH v4 02/39] task_work: Fix TWA_NMI_CURRENT race with __schedule()

In reply to Josh Poimboeuf
Quoted 19 lines · Josh Poimboeuf

If TWA_NMI_CURRENT task work is queued from an NMI triggered while running in __schedule() with IRQs disabled, task_work_set_notify_irq() ends up inadvertently running on the next scheduled task. So the original task doesn't get its TIF_NOTIFY_RESUME flag set and the task work may get delayed indefinitely, or may not get to run at all.

    __schedule()
	// disable irqs
	    <NMI>
		task_work_add(current, work, TWA_NMI_CURRENT);
	    </NMI>
	// current = next;
	// enable irqs
	    <IRQ>
		task_work_set_notify_irq()
		test_and_set_tsk_thread_flag(current, TIF_NOTIFY_RESUME); // wrong task!
	    </IRQ>
	// original task skips task work on its next return to user (or exit!)
Quoted 2 lines

Fix it by storing the task pointer along with the irq_work struct and passing that task to set_notify_resume().

So I'm a little confused, isn't something like this sufficient?

If we hit before schedule(), all just works as expected, if we hit after schedule(), the task will already have the TIF flag set, and we'll hit the return to user path once it gets scheduled again.

---

kernel/task_work.c +7 -1
index c969f1f26be5..155549c017b2 100644
@@ -9,7 +9,12 @@ static struct callback_head work_exited; /* all we need is ->next == NULL */
 #ifdef CONFIG_IRQ_WORK
 static void task_work_set_notify_irq(struct irq_work *entry)
 {
-	test_and_set_tsk_thread_flag(current, TIF_NOTIFY_RESUME);
+	/*
+	 * no-op IPI
+	 *
+	 * TWA_NMI_CURRENT will already have set the TIF flag, all
+	 * this interrupt does it tickle the return-to-user path.
+	 */
 }
 static DEFINE_PER_CPU(struct irq_work, irq_work_NMI_resume) =
 	IRQ_WORK_INIT_HARD(task_work_set_notify_irq);
@@ -98,6 +103,7 @@ int task_work_add(struct task_struct *task, struct callback_head *work,
 		break;
 #ifdef CONFIG_IRQ_WORK
 	case TWA_NMI_CURRENT:
+		set_tsk_thread_flag(current, TIF_NOTIFY_RESUME);
 		irq_work_queue(this_cpu_ptr(&irq_work_NMI_resume));
 		break;
 #endif
author maintainer
#11 lore raw

Re: [PATCH v4 02/39] task_work: Fix TWA_NMI_CURRENT race with __schedule()

Quoted 26 lines · Peter Zijlstra

So I'm a little confused, isn't something like this sufficient?

If we hit before schedule(), all just works as expected, if we hit after schedule(), the task will already have the TIF flag set, and we'll hit the return to user path once it gets scheduled again.

---

kernel/task_work.c +6 -1
index c969f1f26be5..155549c017b2 100644
@@ -9,7 +9,12 @@ static struct callback_head work_exited; /* all we need is ->next == NULL */
 #ifdef CONFIG_IRQ_WORK
 static void task_work_set_notify_irq(struct irq_work *entry)
 {
-	test_and_set_tsk_thread_flag(current, TIF_NOTIFY_RESUME);
+	/*
+	 * no-op IPI
+	 *
+	 * TWA_NMI_CURRENT will already have set the TIF flag, all
+	 * this interrupt does it tickle the return-to-user path.
+	 */
 }
 static DEFINE_PER_CPU(struct irq_work, irq_work_NMI_resume) =
 	IRQ_WORK_INIT_HARD(task_work_set_notify_irq);

kernel/task_work.c int task_work_add(struct task_struct *task, struct callback_head *work, · line 109

Quoted 7 lines
kernel/task_work.c +1 -0
@@ -98,6 +103,7 @@ int task_work_add(struct task_struct *task, struct callback_head *work,
 		break;
 #ifdef CONFIG_IRQ_WORK
 	case TWA_NMI_CURRENT:
+		set_tsk_thread_flag(current, TIF_NOTIFY_RESUME);
 		irq_work_queue(this_cpu_ptr(&irq_work_NMI_resume));
 		break;
 #endif

Yeah, that looks so much better...

The self-IPI is only needed when the NMI happened in user space, right? Would it make sense to have an optimized version of that?

Signature · 1 line
Josh
author maintainer
#12 lore raw

Re: [PATCH v4 02/39] task_work: Fix TWA_NMI_CURRENT race with __schedule()

Quoted 2 lines · Josh Poimboeuf

The self-IPI is only needed when the NMI happened in user space, right? Would it make sense to have an optimized version of that?

Actually, maybe not, that could be tricky if the NMI hits in the kernel after task work runs.

Signature · 1 line
Josh
maintainer
#13 lore raw

Re: [PATCH v4 02/39] task_work: Fix TWA_NMI_CURRENT race with __schedule()

Quoted 6 lines · Josh Poimboeuf
Quoted 2 lines · Josh Poimboeuf

The self-IPI is only needed when the NMI happened in user space, right? Would it make sense to have an optimized version of that?

Actually, maybe not, that could be tricky if the NMI hits in the kernel after task work runs.

Right, I was going to say, lets keep this as simple as possible :-)

maintainer
#14 lore raw

Re: [PATCH v4 02/39] task_work: Fix TWA_NMI_CURRENT race with __schedule()

In reply to Peter Zijlstra
Quoted 24 lines · On Wed, 22 Jan 2025 13:42:28 +0100 Peter Zijlstra <[email protected]> wrote:

If we hit before schedule(), all just works as expected, if we hit after schedule(), the task will already have the TIF flag set, and we'll hit the return to user path once it gets scheduled again.

---

kernel/task_work.c +6 -1
index c969f1f26be5..155549c017b2 100644
@@ -9,7 +9,12 @@ static struct callback_head work_exited; /* all we need is ->next == NULL */
 #ifdef CONFIG_IRQ_WORK
 static void task_work_set_notify_irq(struct irq_work *entry)
 {
-	test_and_set_tsk_thread_flag(current, TIF_NOTIFY_RESUME);
+	/*
+	 * no-op IPI
+	 *
+	 * TWA_NMI_CURRENT will already have set the TIF flag, all
+	 * this interrupt does it tickle the return-to-user path.
+	 */
 }
 static DEFINE_PER_CPU(struct irq_work, irq_work_NMI_resume) =
 	IRQ_WORK_INIT_HARD(task_work_set_notify_irq);

kernel/task_work.c int task_work_add(struct task_struct *task, struct callback_head *work, · line 109

Quoted 7 lines
kernel/task_work.c +1 -0
@@ -98,6 +103,7 @@ int task_work_add(struct task_struct *task, struct callback_head *work,
 		break;
 #ifdef CONFIG_IRQ_WORK
 	case TWA_NMI_CURRENT:
+		set_tsk_thread_flag(current, TIF_NOTIFY_RESUME);
 		irq_work_queue(this_cpu_ptr(&irq_work_NMI_resume));
 		break;
 #endif

Does this patch replace patches 1 and 2?

If so, Peter, can you give me a Signed-off-by?

-- Steve

author maintainer
#15 lore raw

Re: [PATCH v4 02/39] task_work: Fix TWA_NMI_CURRENT race with __schedule()

Quoted 28 lines · Steven Rostedt
Quoted 25 lines · On Wed, 22 Jan 2025 13:42:28 +0100 Peter Zijlstra <[email protected]> wrote:

If we hit before schedule(), all just works as expected, if we hit after schedule(), the task will already have the TIF flag set, and we'll hit the return to user path once it gets scheduled again.

---

kernel/task_work.c +6 -1
index c969f1f26be5..155549c017b2 100644
@@ -9,7 +9,12 @@ static struct callback_head work_exited; /* all we need is ->next == NULL */
 #ifdef CONFIG_IRQ_WORK
 static void task_work_set_notify_irq(struct irq_work *entry)
 {
-	test_and_set_tsk_thread_flag(current, TIF_NOTIFY_RESUME);
+	/*
+	 * no-op IPI
+	 *
+	 * TWA_NMI_CURRENT will already have set the TIF flag, all
+	 * this interrupt does it tickle the return-to-user path.
+	 */
 }
 static DEFINE_PER_CPU(struct irq_work, irq_work_NMI_resume) =
 	IRQ_WORK_INIT_HARD(task_work_set_notify_irq);
@@ -98,6 +103,7 @@ int task_work_add(struct task_struct *task, struct callback_head *work,
 		break;
Quoted 8 lines

kernel/task_work.c int task_work_add(struct task_struct *task, struct callback_head *work, · line 109

Quoted 6 lines
kernel/task_work.c +1 -0
 #ifdef CONFIG_IRQ_WORK
 	case TWA_NMI_CURRENT:
+		set_tsk_thread_flag(current, TIF_NOTIFY_RESUME);
 		irq_work_queue(this_cpu_ptr(&irq_work_NMI_resume));
 		break;
 #endif

Does this patch replace patches 1 and 2?

Indeed it does.

Quoted 3 lines

If so, Peter, can you give me a Signed-off-by?

-- Steve

Signature · 1 line
Josh
author maintainer
#16 lore raw

PATCH v4 3/39 mm: Add guard for mmap_read_lock

In reply to Josh Poimboeuf

This is the new way of doing things. Converting all existing mmap_read_lock users is an exercise left for the reader ;-)

---
 include/linux/mmap_lock.h | 2 ++
 1 file changed, 2 insertions(+)
include/linux/mmap_lock.h +2 -0
index de9dc20b01ba..c971c4617060 100644
@@ -182,4 +182,6 @@ static inline int mmap_lock_is_contended(struct mm_struct *mm)
 	return rwsem_is_contended(&mm->mmap_lock);
 }
 
+DEFINE_GUARD(mmap_read_lock, struct mm_struct *, mmap_read_lock(_T), mmap_read_unlock(_T))
+
 #endif /* _LINUX_MMAP_LOCK_H */
Signature · 1 line
2.48.1
author maintainer
#17 lore raw

PATCH v4 4/39 x86/vdso: Fix DWARF generation for getrandom()

In reply to Josh Poimboeuf

Add CFI annotations to the VDSO implementation of getrandom() so it will have valid DWARF unwinding metadata.

Fixes 33385150ac45 ("x86: vdso: Wire up getrandom() vDSO implementation")
---
 arch/x86/entry/vdso/vgetrandom-chacha.S | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)
arch/x86/entry/vdso/vgetrandom-chacha.S +3 -2
index bcba5639b8ee..cc82da9216fb 100644
@@ -4,7 +4,7 @@
  */
 
 #include <linux/linkage.h>
-#include <asm/frame.h>
+#include <asm/dwarf2.h>
 
 .section	.rodata, "a"
 .align 16
@@ -22,7 +22,7 @@ CONSTANTS:	.octa 0x6b20657479622d323320646e61707865
  *	rcx: number of 64-byte blocks to write to output
  */
 SYM_FUNC_START(__arch_chacha20_blocks_nostack)
-
+	CFI_STARTPROC
 .set	output,		%rdi
 .set	key,		%rsi
 .set	counter,	%rdx
@@ -175,4 +175,5 @@ SYM_FUNC_START(__arch_chacha20_blocks_nostack)
 	pxor		temp,temp
 
 	ret
+	CFI_ENDPROC
 SYM_FUNC_END(__arch_chacha20_blocks_nostack)
Signature · 1 line
2.48.1
author maintainer
#18 lore raw

PATCH v4 5/39 x86/asm: Avoid emitting DWARF CFI for non-VDSO

In reply to Josh Poimboeuf

It was decided years ago that .cfi_* annotations aren't maintainable in the kernel. They were replaced by objtool unwind hints. For the kernel proper, ensure the CFI_* macros don't do anything.

On the other hand the VDSO library *does* use them, so user space can unwind through it.

Make sure these macros only work for VDSO. They aren't actually being used outside of VDSO anyway, so there's no functional change.

---
 arch/x86/include/asm/dwarf2.h | 51 ++++++++++++++++++++++++-----------
 1 file changed, 35 insertions(+), 16 deletions(-)
arch/x86/include/asm/dwarf2.h +35 -16
index 430fca13bb56..b195b3c8677e 100644
@@ -6,6 +6,15 @@
 #warning "asm/dwarf2.h should be only included in pure assembly files"
 #endif
 
+#ifdef BUILD_VDSO
+
+	/*
+	 * For the vDSO, emit both runtime unwind information and debug
+	 * symbols for the .dbg file.
+	 */
+
+	.cfi_sections .eh_frame, .debug_frame
+
 #define CFI_STARTPROC		.cfi_startproc
 #define CFI_ENDPROC		.cfi_endproc
 #define CFI_DEF_CFA		.cfi_def_cfa
@@ -21,21 +30,31 @@
 #define CFI_UNDEFINED		.cfi_undefined
 #define CFI_ESCAPE		.cfi_escape
 
-#ifndef BUILD_VDSO
-	/*
-	 * Emit CFI data in .debug_frame sections, not .eh_frame sections.
-	 * The latter we currently just discard since we don't do DWARF
-	 * unwinding at runtime.  So only the offline DWARF information is
-	 * useful to anyone.  Note we should not use this directive if we
-	 * ever decide to enable DWARF unwinding at runtime.
-	 */
-	.cfi_sections .debug_frame
-#else
-	 /*
-	  * For the vDSO, emit both runtime unwind information and debug
-	  * symbols for the .dbg file.
-	  */
-	.cfi_sections .eh_frame, .debug_frame
-#endif
+#else /* !BUILD_VDSO */
+
+/*
+ * On x86, these macros aren't used outside VDSO.  As well they shouldn't be:
+ * they're fragile and very difficult to maintain.
+ */
+
+.macro nocfi args:vararg
+.endm
+
+#define CFI_STARTPROC		nocfi
+#define CFI_ENDPROC		nocfi
+#define CFI_DEF_CFA		nocfi
+#define CFI_DEF_CFA_REGISTER	nocfi
+#define CFI_DEF_CFA_OFFSET	nocfi
+#define CFI_ADJUST_CFA_OFFSET	nocfi
+#define CFI_OFFSET		nocfi
+#define CFI_REL_OFFSET		nocfi
+#define CFI_REGISTER		nocfi
+#define CFI_RESTORE		nocfi
+#define CFI_REMEMBER_STATE	nocfi
+#define CFI_RESTORE_STATE	nocfi
+#define CFI_UNDEFINED		nocfi
+#define CFI_ESCAPE		nocfi
+
+#endif /* !BUILD_VDSO */
 
 #endif /* _ASM_X86_DWARF2_H */
Signature · 1 line
2.48.1
#19 lore raw

Re: [PATCH v4 05/39] x86/asm: Avoid emitting DWARF CFI for non-VDSO

Quoted 22 lines · Josh Poimboeuf

It was decided years ago that .cfi_* annotations aren't maintainable in the kernel. They were replaced by objtool unwind hints. For the kernel proper, ensure the CFI_* macros don't do anything.

On the other hand the VDSO library *does* use them, so user space can unwind through it.

Make sure these macros only work for VDSO. They aren't actually being used outside of VDSO anyway, so there's no functional change.

arch/x86/include/asm/dwarf2.h +0 -8
 
-#ifndef BUILD_VDSO
-	/*
-	 * Emit CFI data in .debug_frame sections, not .eh_frame sections.
-	 * The latter we currently just discard since we don't do DWARF
-	 * unwinding at runtime.  So only the offline DWARF information is
-	 * useful to anyone.  Note we should not use this directive if we
-	 * ever decide to enable DWARF unwinding at runtime.
-	 */

arch/x86/include/asm/dwarf2.h line 34

Quoted 10 lines
arch/x86/include/asm/dwarf2.h +2 -8
-	.cfi_sections .debug_frame
-#else
-	 /*
-	  * For the vDSO, emit both runtime unwind information and debug
-	  * symbols for the .dbg file.
-	  */
-	.cfi_sections .eh_frame, .debug_frame
-#endif
+#else /* !BUILD_VDSO */
+

Did you remove ".cfi_sections .debug_frame" on purpose from the !BUILD_VDSO path compared to V3? Presumably to not only not emit DWARF CFI from assembler, but any source?

Quoted 4 lines

+/*
+ * On x86, these macros aren't used outside VDSO. As well they shouldn't be:
+ * they're fragile and very difficult to maintain.
+ */

Thanks and regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#20 lore raw

Re: [PATCH v4 05/39] x86/asm: Avoid emitting DWARF CFI for non-VDSO

Quoted 14 lines · Jens Remus
Quoted 13 lines · Josh Poimboeuf
arch/x86/include/asm/dwarf2.h +0 -13
-#ifndef BUILD_VDSO
-	/*
-	 * Emit CFI data in .debug_frame sections, not .eh_frame sections.
-	 * The latter we currently just discard since we don't do DWARF
-	 * unwinding at runtime.  So only the offline DWARF information is
-	 * useful to anyone.  Note we should not use this directive if we
-	 * ever decide to enable DWARF unwinding at runtime.
-	 */
-	.cfi_sections .debug_frame
-#else
-	 /*
-	  * For the vDSO, emit both runtime unwind information and debug
-	  * symbols for the .dbg file.
Quoted 8 lines

arch/x86/include/asm/dwarf2.h line 34

Quoted 5 lines
arch/x86/include/asm/dwarf2.h +2 -3
-	  */
-	.cfi_sections .eh_frame, .debug_frame
-#endif
+#else /* !BUILD_VDSO */
+

Did you remove ".cfi_sections .debug_frame" on purpose from the !BUILD_VDSO path compared to V3?

Yes, since non-VDSO assembly files won't be emitting any .cfi, there's no .debug_frame to output anyway.

Quoted 2 lines

Presumably to not only not emit DWARF CFI from assembler, but any source?

This only impacts assembly files, notice the __ASSEMBLY__ check at the top of the file.

Signature · 1 line
Josh
author maintainer
#21 lore raw

PATCH v4 6/39 x86/asm: Fix VDSO DWARF generation with kernel IBT enabled

In reply to Josh Poimboeuf

The DWARF .cfi_startproc annotation needs to be at the very beginning of a function. But with kernel IBT that doesn't happen as ENDBR is sneakily embedded in SYM_FUNC_START. As a result the DWARF unwinding info is wrong at the beginning of all the VDSO functions.

Fix it by adding CFI_STARTPROC and CFI_ENDPROC to SYM_FUNC_START_* and SYM_FUNC_END respectively. Note this only affects VDSO, as the CFI_* macros are empty for the kernel proper.

Fixes c4691712b546 ("x86/linkage: Add ENDBR to SYM_FUNC_START*()")
---
 arch/x86/entry/vdso/vdso-layout.lds.S   |  2 +-
 arch/x86/entry/vdso/vgetrandom-chacha.S |  2 --
 arch/x86/entry/vdso/vsgx.S              |  4 ----
 arch/x86/include/asm/linkage.h          | 29 +++++++++++++++++++------
 arch/x86/include/asm/vdso.h             |  1 -
 5 files changed, 23 insertions(+), 15 deletions(-)
arch/x86/entry/vdso/vdso-layout.lds.S +1 -1
index 872947c1004c..506c9800a5aa 100644
@@ -1,5 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0 */
-#include <asm/vdso.h>
+#include <asm/page_types.h>
 #include <asm/vdso/vsyscall.h>
 
 /*
arch/x86/entry/vdso/vgetrandom-chacha.S +0 -2
index cc82da9216fb..a33212594731 100644
@@ -22,7 +22,6 @@ CONSTANTS:	.octa 0x6b20657479622d323320646e61707865
  *	rcx: number of 64-byte blocks to write to output
  */
 SYM_FUNC_START(__arch_chacha20_blocks_nostack)
-	CFI_STARTPROC
 .set	output,		%rdi
 .set	key,		%rsi
 .set	counter,	%rdx
@@ -175,5 +174,4 @@ SYM_FUNC_START(__arch_chacha20_blocks_nostack)
 	pxor		temp,temp
 
 	ret
-	CFI_ENDPROC
 SYM_FUNC_END(__arch_chacha20_blocks_nostack)
arch/x86/entry/vdso/vsgx.S +0 -4
index 37a3d4c02366..c0342238c976 100644
@@ -24,8 +24,6 @@
 .section .text, "ax"
 
 SYM_FUNC_START(__vdso_sgx_enter_enclave)
-	/* Prolog */
-	.cfi_startproc
 	push	%rbp
 	.cfi_adjust_cfa_offset	8
 	.cfi_rel_offset		%rbp, 0
@@ -143,8 +141,6 @@ SYM_FUNC_START(__vdso_sgx_enter_enclave)
 	jle	.Lout
 	jmp	.Lenter_enclave
 
-	.cfi_endproc
-
 _ASM_VDSO_EXTABLE_HANDLE(.Lenclu_eenter_eresume, .Lhandle_exception)
 
 SYM_FUNC_END(__vdso_sgx_enter_enclave)
arch/x86/include/asm/linkage.h +22 -7
index dc31b13b87a0..2866d57ef907 100644
@@ -40,6 +40,10 @@
 
 #ifdef __ASSEMBLY__
 
+#ifndef LINKER_SCRIPT
+#include <asm/dwarf2.h>
+#endif
+
 #if defined(CONFIG_MITIGATION_RETHUNK) && !defined(__DISABLE_EXPORTS) && !defined(BUILD_VDSO)
 #define RET	jmp __x86_return_thunk
 #else /* CONFIG_MITIGATION_RETPOLINE */
@@ -112,40 +116,51 @@
 # define SYM_FUNC_ALIAS_MEMFUNC	SYM_FUNC_ALIAS
 #endif
 
+#define __SYM_FUNC_START				\
+	CFI_STARTPROC ASM_NL				\
+	ENDBR
+
+#define __SYM_FUNC_END					\
+	CFI_ENDPROC ASM_NL
+
 /* SYM_TYPED_FUNC_START -- use for indirectly called globals, w/ CFI type */
 #define SYM_TYPED_FUNC_START(name)				\
 	SYM_TYPED_START(name, SYM_L_GLOBAL, SYM_F_ALIGN)	\
-	ENDBR
+	__SYM_FUNC_START
 
 /* SYM_FUNC_START -- use for global functions */
 #define SYM_FUNC_START(name)				\
 	SYM_START(name, SYM_L_GLOBAL, SYM_F_ALIGN)	\
-	ENDBR
+	__SYM_FUNC_START
 
 /* SYM_FUNC_START_NOALIGN -- use for global functions, w/o alignment */
 #define SYM_FUNC_START_NOALIGN(name)			\
 	SYM_START(name, SYM_L_GLOBAL, SYM_A_NONE)	\
-	ENDBR
+	__SYM_FUNC_START
 
 /* SYM_FUNC_START_LOCAL -- use for local functions */
 #define SYM_FUNC_START_LOCAL(name)			\
 	SYM_START(name, SYM_L_LOCAL, SYM_F_ALIGN)	\
-	ENDBR
+	__SYM_FUNC_START
 
 /* SYM_FUNC_START_LOCAL_NOALIGN -- use for local functions, w/o alignment */
 #define SYM_FUNC_START_LOCAL_NOALIGN(name)		\
 	SYM_START(name, SYM_L_LOCAL, SYM_A_NONE)	\
-	ENDBR
+	__SYM_FUNC_START
 
 /* SYM_FUNC_START_WEAK -- use for weak functions */
 #define SYM_FUNC_START_WEAK(name)			\
 	SYM_START(name, SYM_L_WEAK, SYM_F_ALIGN)	\
-	ENDBR
+	__SYM_FUNC_START
 
 /* SYM_FUNC_START_WEAK_NOALIGN -- use for weak functions, w/o alignment */
 #define SYM_FUNC_START_WEAK_NOALIGN(name)		\
 	SYM_START(name, SYM_L_WEAK, SYM_A_NONE)		\
-	ENDBR
+	__SYM_FUNC_START
+
+#define SYM_FUNC_END(name)				\
+	__SYM_FUNC_END					\
+	SYM_END(name, SYM_T_FUNC)
 
 #endif /* _ASM_X86_LINKAGE_H */
 
arch/x86/include/asm/vdso.h +0 -1
index d7f6592b74a9..0111c349bbc5 100644
@@ -2,7 +2,6 @@
 #ifndef _ASM_X86_VDSO_H
 #define _ASM_X86_VDSO_H
 
-#include <asm/page_types.h>
 #include <linux/linkage.h>
 #include <linux/init.h>
 
Signature · 1 line
2.48.1
author maintainer
#22 lore raw

PATCH v4 7/39 x86/vdso: Use SYM_FUNC_{START,END} in __kernel_vsyscall()

In reply to Josh Poimboeuf

Use SYM_FUNC_{START,END} instead of all the boilerplate. No functional change.

---
 arch/x86/entry/vdso/vdso32/system_call.S | 10 ++--------
 1 file changed, 2 insertions(+), 8 deletions(-)
arch/x86/entry/vdso/vdso32/system_call.S +2 -8
index d33c6513fd2c..bdc576548240 100644
@@ -9,11 +9,7 @@
 #include <asm/alternative.h>
 
 	.text
-	.globl __kernel_vsyscall
-	.type __kernel_vsyscall,@function
-	ALIGN
-__kernel_vsyscall:
-	CFI_STARTPROC
+SYM_FUNC_START(__kernel_vsyscall)
 	/*
 	 * Reshuffle regs so that all of any of the entry instructions
 	 * will preserve enough state.
@@ -79,7 +75,5 @@ SYM_INNER_LABEL(int80_landing_pad, SYM_L_GLOBAL)
 	CFI_RESTORE		ecx
 	CFI_ADJUST_CFA_OFFSET	-4
 	RET
-	CFI_ENDPROC
-
-	.size __kernel_vsyscall,.-__kernel_vsyscall
+SYM_FUNC_END(__kernel_vsyscall)
 	.previous
Signature · 1 line
2.48.1
author maintainer
#23 lore raw

PATCH v4 8/39 x86/vdso: Use CFI macros in __vdso_sgx_enter_enclave()

In reply to Josh Poimboeuf

Use the CFI macros instead of the raw .cfi_* directives to be consistent with the rest of the VDSO asm. It's also easier on the eyes.

No functional changes.

---
 arch/x86/entry/vdso/vsgx.S | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)
arch/x86/entry/vdso/vsgx.S +7 -8
index c0342238c976..8d7b8eb45c50 100644
@@ -24,13 +24,14 @@
 .section .text, "ax"
 
 SYM_FUNC_START(__vdso_sgx_enter_enclave)
+	SYM_F_ALIGN
 	push	%rbp
-	.cfi_adjust_cfa_offset	8
-	.cfi_rel_offset		%rbp, 0
+	CFI_ADJUST_CFA_OFFSET	8
+	CFI_REL_OFFSET		%rbp, 0
 	mov	%rsp, %rbp
-	.cfi_def_cfa_register	%rbp
+	CFI_DEF_CFA_REGISTER	%rbp
 	push	%rbx
-	.cfi_rel_offset		%rbx, -8
+	CFI_REL_OFFSET		%rbx, -8
 
 	mov	%ecx, %eax
 .Lenter_enclave:
@@ -77,13 +78,11 @@ SYM_FUNC_START(__vdso_sgx_enter_enclave)
 .Lout:
 	pop	%rbx
 	leave
-	.cfi_def_cfa		%rsp, 8
+	CFI_DEF_CFA		%rsp, 8
 	RET
 
-	/* The out-of-line code runs with the pre-leave stack frame. */
-	.cfi_def_cfa		%rbp, 16
-
 .Linvalid_input:
+	CFI_DEF_CFA		%rbp, 16
 	mov	$(-EINVAL), %eax
 	jmp	.Lout
 
Signature · 1 line
2.48.1
author maintainer
#24 lore raw

PATCH v4 9/39 x86/vdso: Enable sframe generation in VDSO

In reply to Josh Poimboeuf

Enable sframe generation in the VDSO library so kernel and user space can unwind through it.

---
 arch/Kconfig                          |  3 +++
 arch/x86/entry/vdso/Makefile          | 10 +++++++---
 arch/x86/entry/vdso/vdso-layout.lds.S |  3 +++
 arch/x86/include/asm/dwarf2.h         |  5 ++++-
 4 files changed, 17 insertions(+), 4 deletions(-)
arch/Kconfig +3 -0
index 6682b2a53e34..65228c78fef0 100644
@@ -435,6 +435,9 @@ config HAVE_HARDLOCKUP_DETECTOR_ARCH
 	  It uses the same command line parameters, and sysctl interface,
 	  as the generic hardlockup detectors.
 
+config AS_SFRAME
+	def_bool $(as-instr,.cfi_sections .sframe\n.cfi_startproc\n.cfi_endproc)
+
 config HAVE_PERF_REGS
 	bool
 	help
arch/x86/entry/vdso/Makefile +7 -3
index c9216ac4fb1e..478de89029d1 100644
@@ -47,13 +47,17 @@ quiet_cmd_vdso2c = VDSO2C  $@
 $(obj)/vdso-image-%.c: $(obj)/vdso%.so.dbg $(obj)/vdso%.so $(obj)/vdso2c FORCE
 	$(call if_changed,vdso2c)
 
+#ifdef CONFIG_AS_SFRAME
+SFRAME_CFLAGS := -Wa$(comma)-gsframe
+#endif
+
 #
 # Don't omit frame pointers for ease of userspace debugging, but do
 # optimize sibling calls.
 #
 CFL := $(PROFILING) -mcmodel=small -fPIC -O2 -fasynchronous-unwind-tables -m64 \
        $(filter -g%,$(KBUILD_CFLAGS)) -fno-stack-protector \
-       -fno-omit-frame-pointer -foptimize-sibling-calls \
+       -fno-omit-frame-pointer $(SFRAME_CFLAGS) -foptimize-sibling-calls \
        -DDISABLE_BRANCH_PROFILING -DBUILD_VDSO
 
 ifdef CONFIG_MITIGATION_RETPOLINE
@@ -63,7 +67,7 @@ endif
 endif
 
 $(vobjs): KBUILD_CFLAGS := $(filter-out $(PADDING_CFLAGS) $(CC_FLAGS_LTO) $(CC_FLAGS_CFI) $(RANDSTRUCT_CFLAGS) $(GCC_PLUGINS_CFLAGS) $(RETPOLINE_CFLAGS),$(KBUILD_CFLAGS)) $(CFL)
-$(vobjs): KBUILD_AFLAGS += -DBUILD_VDSO
+$(vobjs): KBUILD_AFLAGS += -DBUILD_VDSO $(SFRAME_CFLAGS)
 
 #
 # vDSO code runs in userspace and -pg doesn't help with profiling anyway.
@@ -104,7 +108,7 @@ $(obj)/%-x32.o: $(obj)/%.o FORCE
 
 targets += vdsox32.lds $(vobjx32s-y)
 
-$(obj)/%.so: OBJCOPYFLAGS := -S --remove-section __ex_table
+$(obj)/%.so: OBJCOPYFLAGS := -g --remove-section __ex_table
 $(obj)/%.so: $(obj)/%.so.dbg FORCE
 	$(call if_changed,objcopy)
 
arch/x86/entry/vdso/vdso-layout.lds.S +3 -0
index 506c9800a5aa..4dcde4747b07 100644
@@ -63,6 +63,7 @@ SECTIONS
 	.eh_frame_hdr	: { *(.eh_frame_hdr) }		:text	:eh_frame_hdr
 	.eh_frame	: { KEEP (*(.eh_frame)) }	:text
 
+	.sframe		: { *(.sframe) }		:text	:sframe
 
 	/*
 	 * Text is well-separated from actual data: there's plenty of
@@ -91,6 +92,7 @@ SECTIONS
  * Very old versions of ld do not recognize this name token; use the constant.
  */
 #define PT_GNU_EH_FRAME	0x6474e550
+#define PT_GNU_SFRAME	0x6474e554
 
 /*
  * We must supply the ELF program headers explicitly to get just one
@@ -102,4 +104,5 @@ PHDRS
 	dynamic		PT_DYNAMIC	FLAGS(4);		/* PF_R */
 	note		PT_NOTE		FLAGS(4);		/* PF_R */
 	eh_frame_hdr	PT_GNU_EH_FRAME;
+	sframe		PT_GNU_SFRAME;
 }
arch/x86/include/asm/dwarf2.h +4 -1
index b195b3c8677e..1c354f648505 100644
@@ -12,8 +12,11 @@
 	 * For the vDSO, emit both runtime unwind information and debug
 	 * symbols for the .dbg file.
 	 */
-
+#ifdef __x86_64__
+	.cfi_sections .eh_frame, .debug_frame, .sframe
+#else
 	.cfi_sections .eh_frame, .debug_frame
+#endif
 
 #define CFI_STARTPROC		.cfi_startproc
 #define CFI_ENDPROC		.cfi_endproc
Signature · 1 line
2.48.1
#25 lore raw

Re: [PATCH v4 09/39] x86/vdso: Enable sframe generation in VDSO

Quoted 4 lines · Josh Poimboeuf

Enable sframe generation in the VDSO library so kernel and user space can unwind through it.

...

arch/x86/include/asm/dwarf2.h line 15

Quoted 9 lines
arch/x86/include/asm/dwarf2.h +0 -1
index b195b3c8677e..1c354f648505 100644
@@ -12,8 +12,11 @@
  	 * For the vDSO, emit both runtime unwind information and debug
  	 * symbols for the .dbg file.
  	 */
-

Nit: Deleted blank line you introduced in "[PATCH v4 05/39] x86/asm:
Avoid emitting DWARF CFI for non-VDSO".

arch/x86/include/asm/dwarf2.h line 15

Quoted 1 line
arch/x86/include/asm/dwarf2.h +1 -0
+#ifdef __x86_64__

#if defined(__x86_64__) && defined(CONFIG_AS_SFRAME)

AFAIK the kernel has a minimum binutils requirement of 2.25 [1] and assembler option "--gsframe" as well as directive ".cfi_sections .sframe" were introduced with 2.40.

arch/x86/include/asm/dwarf2.h line 22

Quoted 7 lines
arch/x86/include/asm/dwarf2.h +3 -0
+	.cfi_sections .eh_frame, .debug_frame, .sframe
+#else
  	.cfi_sections .eh_frame, .debug_frame
+#endif
 
  #define CFI_STARTPROC		.cfi_startproc
  #define CFI_ENDPROC		.cfi_endproc

[1]: https://docs.kernel.org/process/changes.html

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#26 lore raw

Re: [PATCH v4 09/39] x86/vdso: Enable sframe generation in VDSO

Quoted 13 lines · Jens Remus

arch/x86/include/asm/dwarf2.h line 15

Quoted 9 lines · Josh Poimboeuf
arch/x86/include/asm/dwarf2.h +0 -1
index b195b3c8677e..1c354f648505 100644
@@ -12,8 +12,11 @@
  	 * For the vDSO, emit both runtime unwind information and debug
  	 * symbols for the .dbg file.
  	 */
-

Nit: Deleted blank line you introduced in "[PATCH v4 05/39] x86/asm:
Avoid emitting DWARF CFI for non-VDSO".

Indeed.

Quoted 7 lines

arch/x86/include/asm/dwarf2.h line 15

Quoted 1 line
arch/x86/include/asm/dwarf2.h +1 -0
+#ifdef __x86_64__

#if defined(__x86_64__) && defined(CONFIG_AS_SFRAME)

AFAIK the kernel has a minimum binutils requirement of 2.25 [1] and assembler option "--gsframe" as well as directive ".cfi_sections .sframe" were introduced with 2.40.

True, I'll change it to just '#ifdef CONFIG_AS_SFRAME' since that's what really matters (and 32-bit doesn't support it anyway).

Quoted 6 lines
Quoted 6 lines
arch/x86/include/asm/dwarf2.h +3 -0
+	.cfi_sections .eh_frame, .debug_frame, .sframe
+#else
  	.cfi_sections .eh_frame, .debug_frame
+#endif
  #define CFI_STARTPROC		.cfi_startproc
  #define CFI_ENDPROC		.cfi_endproc
Signature · 1 line
Josh
author maintainer
#27 lore raw

Re: [PATCH v4 09/39] x86/vdso: Enable sframe generation in VDSO

Quoted 4 lines · Josh Poimboeuf
Quoted 3 lines · Jens Remus
Quoted 1 line · Josh Poimboeuf

+#ifdef __x86_64__

Quoted 8 lines
Quoted 5 lines

#if defined(__x86_64__) && defined(CONFIG_AS_SFRAME)

AFAIK the kernel has a minimum binutils requirement of 2.25 [1] and assembler option "--gsframe" as well as directive ".cfi_sections .sframe" were introduced with 2.40.

True, I'll change it to just '#ifdef CONFIG_AS_SFRAME' since that's what really matters (and 32-bit doesn't support it anyway).

Actually, just CONFIG_AS_CFAME didn't work as 64-bit still compiles some 32-bit asm files. I'll go with your suggestion.

Signature · 1 line
Josh
maintainer
#28 lore raw

Re: [PATCH v4 09/39] x86/vdso: Enable sframe generation in VDSO

In reply to Josh Poimboeuf
Quoted 15 lines · On Fri, 24 Jan 2025 08:43:32 -0800 Josh Poimboeuf <[email protected]> wrote:
Quoted 13 lines · Jens Remus

arch/x86/include/asm/dwarf2.h line 15

Quoted 9 lines · Josh Poimboeuf
arch/x86/include/asm/dwarf2.h +0 -1
index b195b3c8677e..1c354f648505 100644
@@ -12,8 +12,11 @@
  	 * For the vDSO, emit both runtime unwind information and debug
  	 * symbols for the .dbg file.
  	 */
-  

Nit: Deleted blank line you introduced in "[PATCH v4 05/39] x86/asm:
Avoid emitting DWARF CFI for non-VDSO".

Quoted 1 line

Indeed.

Note, I'm getting ready to send out an update of Josh's patches.

This particular nit isn't really an issue, as #ifdef's do sometimes replace blank lines.

Just mentioning this so you don't think I'm ignoring it, when I post patches.

-- Steve

#29 lore raw

Re: [PATCH v4 09/39] x86/vdso: Enable sframe generation in VDSO

In reply to Josh Poimboeuf
Quoted 8 lines · Josh Poimboeuf

Enable sframe generation in the VDSO library so kernel and user space can unwind through it.

arch/x86/entry/vdso/Makefile quiet_cmd_vdso2c = VDSO2C $@ · line 53

Quoted 7 lines
arch/x86/entry/vdso/Makefile +4 -0
@@ -47,13 +47,17 @@ quiet_cmd_vdso2c = VDSO2C  $@
  $(obj)/vdso-image-%.c: $(obj)/vdso%.so.dbg $(obj)/vdso%.so $(obj)/vdso2c FORCE
  	$(call if_changed,vdso2c)
 
+#ifdef CONFIG_AS_SFRAME
+SFRAME_CFLAGS := -Wa$(comma)-gsframe
+#endif
+

You probably erroneously mixed up C preprocessor and Makefile syntax? :-)

ifeq ($(CONFIG_AS_SFRAME),y)
SFRAME_CFLAGS := -Wa,--gsframe
endif

$(comma) does not appear to be required in this context.

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#30 lore raw

Re: [PATCH v4 09/39] x86/vdso: Enable sframe generation in VDSO

Quoted 18 lines · Jens Remus
Quoted 8 lines · Josh Poimboeuf

Enable sframe generation in the VDSO library so kernel and user space can unwind through it.

arch/x86/entry/vdso/Makefile quiet_cmd_vdso2c = VDSO2C $@ · line 52

Quoted 6 lines
arch/x86/entry/vdso/Makefile +4 -0
@@ -47,13 +47,17 @@ quiet_cmd_vdso2c = VDSO2C  $@
  $(obj)/vdso-image-%.c: $(obj)/vdso%.so.dbg $(obj)/vdso%.so $(obj)/vdso2c FORCE
  	$(call if_changed,vdso2c)
+#ifdef CONFIG_AS_SFRAME
+SFRAME_CFLAGS := -Wa$(comma)-gsframe
+#endif
+

You probably erroneously mixed up C preprocessor and Makefile syntax? :-)

Quoted 5 lines

ifeq ($(CONFIG_AS_SFRAME),y)
SFRAME_CFLAGS := -Wa,--gsframe
endif

$(comma) does not appear to be required in this context.

Yeah, before it was in a Makefile macro which needed the $(comma) to distinguish it from the macro arguments.

Signature · 1 line
Josh
author maintainer
#31 lore raw

PATCH v4 10/39 x86/uaccess: Add unsafe_copy_from_user() implementation

In reply to Josh Poimboeuf

Add an x86 implementation of unsafe_copy_from_user() similar to the existing unsafe_copy_to_user().

---
 arch/x86/include/asm/uaccess.h | 39 +++++++++++++++++++++++++---------
 1 file changed, 29 insertions(+), 10 deletions(-)
arch/x86/include/asm/uaccess.h +29 -10
index 3a7755c1a441..a3148865bc57 100644
@@ -599,23 +599,42 @@ _label:									\
  * We want the unsafe accessors to always be inlined and use
  * the error labels - thus the macro games.
  */
-#define unsafe_copy_loop(dst, src, len, type, label)				\
+#define unsafe_copy_to_user_loop(dst, src, len, type, label)			\
 	while (len >= sizeof(type)) {						\
-		unsafe_put_user(*(type *)(src),(type __user *)(dst),label);	\
+		unsafe_put_user(*(type *)(src), (type __user *)(dst), label);	\
 		dst += sizeof(type);						\
 		src += sizeof(type);						\
 		len -= sizeof(type);						\
 	}
 
-#define unsafe_copy_to_user(_dst,_src,_len,label)			\
+#define unsafe_copy_to_user(_dst, _src, _len, label)			\
 do {									\
-	char __user *__ucu_dst = (_dst);				\
-	const char *__ucu_src = (_src);					\
-	size_t __ucu_len = (_len);					\
-	unsafe_copy_loop(__ucu_dst, __ucu_src, __ucu_len, u64, label);	\
-	unsafe_copy_loop(__ucu_dst, __ucu_src, __ucu_len, u32, label);	\
-	unsafe_copy_loop(__ucu_dst, __ucu_src, __ucu_len, u16, label);	\
-	unsafe_copy_loop(__ucu_dst, __ucu_src, __ucu_len, u8, label);	\
+	void __user *__dst = (_dst);					\
+	const void *__src = (_src);					\
+	size_t __len = (_len);						\
+	unsafe_copy_to_user_loop(__dst, __src, __len, u64, label);	\
+	unsafe_copy_to_user_loop(__dst, __src, __len, u32, label);	\
+	unsafe_copy_to_user_loop(__dst, __src, __len, u16, label);	\
+	unsafe_copy_to_user_loop(__dst, __src, __len, u8,  label);	\
+} while (0)
+
+#define unsafe_copy_from_user_loop(dst, src, len, type, label)			\
+	while (len >= sizeof(type)) {						\
+		unsafe_get_user(*(type *)(dst),(type __user *)(src),label);	\
+		dst += sizeof(type);						\
+		src += sizeof(type);						\
+		len -= sizeof(type);						\
+	}
+
+#define unsafe_copy_from_user(_dst, _src, _len, label)			\
+do {									\
+	void *__dst = (_dst);						\
+	void __user *__src = (_src);					\
+	size_t __len = (_len);						\
+	unsafe_copy_from_user_loop(__dst, __src, __len, u64, label);	\
+	unsafe_copy_from_user_loop(__dst, __src, __len, u32, label);	\
+	unsafe_copy_from_user_loop(__dst, __src, __len, u16, label);	\
+	unsafe_copy_from_user_loop(__dst, __src, __len, u8,  label);	\
 } while (0)
 
 #ifdef CONFIG_CC_HAS_ASM_GOTO_OUTPUT
Signature · 1 line
2.48.1
author maintainer
#32 lore raw

PATCH v4 11/39 unwind_user: Add user space unwinding API

In reply to Josh Poimboeuf

Introduce a generic API for unwinding user stacks.

---
 arch/Kconfig                      |  3 ++
 include/linux/unwind_user.h       | 15 ++++++++
 include/linux/unwind_user_types.h | 31 ++++++++++++++++
 kernel/Makefile                   |  1 +
 kernel/unwind/Makefile            |  1 +
 kernel/unwind/user.c              | 59 +++++++++++++++++++++++++++++++
 6 files changed, 110 insertions(+)
 create mode 100644 include/linux/unwind_user.h
 create mode 100644 include/linux/unwind_user_types.h
 create mode 100644 kernel/unwind/Makefile
 create mode 100644 kernel/unwind/user.c
arch/Kconfig +3 -0
index 65228c78fef0..c6fa2b3ecbc6 100644
@@ -435,6 +435,9 @@ config HAVE_HARDLOCKUP_DETECTOR_ARCH
 	  It uses the same command line parameters, and sysctl interface,
 	  as the generic hardlockup detectors.
 
+config UNWIND_USER
+	bool
+
 config AS_SFRAME
 	def_bool $(as-instr,.cfi_sections .sframe\n.cfi_startproc\n.cfi_endproc)
 
include/linux/unwind_user.h new file +15 -0
new file mode 100644
index 000000000000..aa7923c1384f
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_UNWIND_USER_H
+#define _LINUX_UNWIND_USER_H
+
+#include <linux/unwind_user_types.h>
+
+int unwind_user_start(struct unwind_user_state *state);
+int unwind_user_next(struct unwind_user_state *state);
+
+int unwind_user(struct unwind_stacktrace *trace, unsigned int max_entries);
+
+#define for_each_user_frame(state) \
+	for (unwind_user_start((state)); !(state)->done; unwind_user_next((state)))
+
+#endif /* _LINUX_UNWIND_USER_H */
include/linux/unwind_user_types.h new file +31 -0
new file mode 100644
index 000000000000..6ed1b4ae74e1
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_UNWIND_USER_TYPES_H
+#define _LINUX_UNWIND_USER_TYPES_H
+
+#include <linux/types.h>
+
+enum unwind_user_type {
+	UNWIND_USER_TYPE_NONE,
+};
+
+struct unwind_stacktrace {
+	unsigned int	nr;
+	unsigned long	*entries;
+};
+
+struct unwind_user_frame {
+	s32 cfa_off;
+	s32 ra_off;
+	s32 fp_off;
+	bool use_fp;
+};
+
+struct unwind_user_state {
+	unsigned long ip;
+	unsigned long sp;
+	unsigned long fp;
+	enum unwind_user_type type;
+	bool done;
+};
+
+#endif /* _LINUX_UNWIND_USER_TYPES_H */
kernel/Makefile +1 -0
index 87866b037fbe..6cb4b0e02a34 100644
@@ -50,6 +50,7 @@ obj-y += rcu/
 obj-y += livepatch/
 obj-y += dma/
 obj-y += entry/
+obj-y += unwind/
 obj-$(CONFIG_MODULES) += module/
 
 obj-$(CONFIG_KCMP) += kcmp.o
kernel/unwind/Makefile new file +1 -0
new file mode 100644
index 000000000000..349ce3677526
@@ -0,0 +1 @@
+ obj-$(CONFIG_UNWIND_USER) += user.o
kernel/unwind/user.c new file +59 -0

59 lines not on this page · raw message

Signature · 1 line
2.48.1
#33 lore raw

Re: [PATCH v4 11/39] unwind_user: Add user space unwinding API

Quoted 7 lines · Josh Poimboeuf
kernel/unwind/user.c +4 -0
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+* Generic interfaces for unwinding user space
+*/

kernel/unwind/user.c line 14

Quoted 10 lines
kernel/unwind/user.c +10 -0
+#include <linux/kernel.h>
+#include <linux/sched.h>
+#include <linux/sched/task_stack.h>
+#include <linux/unwind_user.h>
+
+int unwind_user_next(struct unwind_user_state *state)
+{
+	struct unwind_user_frame _frame;
+	struct unwind_user_frame *frame = &_frame;
+	unsigned long cfa = 0, fp, ra = 0;

Why are cfa and ra initialized to zero? Where is that important in subsequent patches?

"[PATCH v4 12/39] unwind_user: Add frame pointer support" does either unconditionally set both cfa and ra or bail out.

kernel/unwind/user.c line 18

Quoted 4 lines
kernel/unwind/user.c +4 -0
+
+	/* no implementation yet */
+	-EINVAL;
+}

Thanks and regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#34 lore raw

Re: [PATCH v4 11/39] unwind_user: Add user space unwinding API

Quoted 10 lines · Jens Remus

kernel/unwind/user.c line 14

Quoted 5 lines · Josh Poimboeuf
kernel/unwind/user.c new file +5 -0
+int unwind_user_next(struct unwind_user_state *state)
+{
+	struct unwind_user_frame _frame;
+	struct unwind_user_frame *frame = &_frame;
+	unsigned long cfa = 0, fp, ra = 0;

Why are cfa and ra initialized to zero? Where is that important in subsequent patches?

Quoted 2 lines

"[PATCH v4 12/39] unwind_user: Add frame pointer support" does either unconditionally set both cfa and ra or bail out.

Right, probably leftovers from some previous iteration. I drop those.

Signature · 1 line
Josh
#35 lore raw

Re: [PATCH v4 11/39] unwind_user: Add user space unwinding API

In reply to Josh Poimboeuf
Quoted 17 lines · Josh Poimboeuf

Introduce a generic API for unwinding user stacks.

---
 arch/Kconfig                      |  3 ++
 include/linux/unwind_user.h       | 15 ++++++++
 include/linux/unwind_user_types.h | 31 ++++++++++++++++
 kernel/Makefile                   |  1 +
 kernel/unwind/Makefile            |  1 +
 kernel/unwind/user.c              | 59 +++++++++++++++++++++++++++++++
 6 files changed, 110 insertions(+)
 create mode 100644 include/linux/unwind_user.h
 create mode 100644 include/linux/unwind_user_types.h
 create mode 100644 kernel/unwind/Makefile
 create mode 100644 kernel/unwind/user.c

[...]

Quoted 7 lines
kernel/unwind/user.c new file +4 -0
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+* Generic interfaces for unwinding user space
+*/

kernel/unwind/user.c line 14

Quoted 10 lines
kernel/unwind/user.c new file +10 -0
+#include <linux/kernel.h>
+#include <linux/sched.h>
+#include <linux/sched/task_stack.h>
+#include <linux/unwind_user.h>
+
+int unwind_user_next(struct unwind_user_state *state)
+{
+       struct unwind_user_frame _frame;
+       struct unwind_user_frame *frame = &_frame;
+       unsigned long cfa = 0, fp, ra = 0;

wouldn't all the above generate compilation warnings about unused variables, potentially breaking bisection?

kernel/unwind/user.c line 17

Quoted 3 lines
kernel/unwind/user.c new file +3 -0
+
+       /* no implementation yet */
+       -EINVAL;

return missing?

Quoted 1 line
kernel/unwind/user.c new file +1 -0
+}

[...]

author maintainer
#36 lore raw

Re: [PATCH v4 11/39] unwind_user: Add user space unwinding API

Quoted 9 lines · Andrii Nakryiko

kernel/unwind/user.c line 14

Quoted 5 lines · Josh Poimboeuf
kernel/unwind/user.c new file +5 -0
+int unwind_user_next(struct unwind_user_state *state)
+{
+       struct unwind_user_frame _frame;
+       struct unwind_user_frame *frame = &_frame;
+       unsigned long cfa = 0, fp, ra = 0;

wouldn't all the above generate compilation warnings about unused variables, potentially breaking bisection?

It doesn't break bisection because no arches support UNWIND_USER yet so this can't yet be compiled.

But yeah, it's shoddy and I fixed it after Jens' comment about the unnecessary zero-initialized variables.

Quoted 6 lines

kernel/unwind/user.c line 17

Quoted 4 lines
kernel/unwind/user.c new file +3 -0
+
+       /* no implementation yet */
+       -EINVAL;

return missing?

heh :-)

Signature · 1 line
Josh
maintainer
#37 lore raw

Re: [PATCH v4 11/39] unwind_user: Add user space unwinding API

In reply to Josh Poimboeuf
Quoted 20 lines · On Tue, 21 Jan 2025 18:31:03 -0800 Josh Poimboeuf <[email protected]> wrote:
kernel/unwind/user.c new file +20 -0
+int unwind_user_next(struct unwind_user_state *state)
+{
+	struct unwind_user_frame _frame;
+	struct unwind_user_frame *frame = &_frame;
+	unsigned long cfa = 0, fp, ra = 0;
+
+	/* no implementation yet */
+	-EINVAL;
+}
+
+int unwind_user_start(struct unwind_user_state *state)
+{
+	struct pt_regs *regs = task_pt_regs(current);
+
+	memset(state, 0, sizeof(*state));
+
+	if (!current->mm || !user_mode(regs)) {
+		state->done = true;
+		return -EINVAL;
+	}

kernel/unwind/user.c line 39

Quoted 10 lines
kernel/unwind/user.c new file +10 -0
+
+	state->type = UNWIND_USER_TYPE_NONE;
+
+	state->ip = instruction_pointer(regs);
+	state->sp = user_stack_pointer(regs);
+	state->fp = frame_pointer(regs);
+
+	return 0;
+}
+

I know this is just an introductory of the interface, but this should really have kerneldoc attached to it, as I have no idea what these are supposed to be doing. This patch is meaningless without it. The change log is useless too.

-- Steve

Quoted 20 lines
kernel/unwind/user.c new file +20 -0
+int unwind_user(struct unwind_stacktrace *trace, unsigned int max_entries)
+{
+	struct unwind_user_state state;
+
+	trace->nr = 0;
+
+	if (!max_entries)
+		return -EINVAL;
+
+	if (!current->mm)
+		return 0;
+
+	for_each_user_frame(&state) {
+		trace->entries[trace->nr++] = state.ip;
+		if (trace->nr >= max_entries)
+			break;
+	}
+
+	return 0;
+}
author maintainer
#38 lore raw

Re: [PATCH v4 11/39] unwind_user: Add user space unwinding API

Quoted 19 lines · Steven Rostedt
Quoted 17 lines · On Tue, 21 Jan 2025 18:31:03 -0800 Josh Poimboeuf <[email protected]> wrote:
kernel/unwind/user.c new file +17 -0
+int unwind_user_start(struct unwind_user_state *state)
+{
+	struct pt_regs *regs = task_pt_regs(current);
+
+	memset(state, 0, sizeof(*state));
+
+	if (!current->mm || !user_mode(regs)) {
+		state->done = true;
+		return -EINVAL;
+	}
+
+	state->type = UNWIND_USER_TYPE_NONE;
+
+	state->ip = instruction_pointer(regs);
+	state->sp = user_stack_pointer(regs);
+	state->fp = frame_pointer(regs);
+
Quoted 8 lines

kernel/unwind/user.c line 39

Quoted 3 lines
kernel/unwind/user.c new file +3 -0
+	return 0;
+}
+

I know this is just an introductory of the interface, but this should really have kerneldoc attached to it, as I have no idea what these are supposed to be doing. This patch is meaningless without it. The change log is useless too.

Yeah, sure.

Signature · 1 line
Josh
author maintainer
#39 lore raw

PATCH v4 12/39 unwind_user: Add frame pointer support

In reply to Josh Poimboeuf

Add optional support for user space frame pointer unwinding. If supported, the arch needs to enable CONFIG_HAVE_UNWIND_USER_FP and define ARCH_INIT_USER_FP_FRAME.

By encoding the frame offsets in struct unwind_user_frame, much of this code can also be reused for future unwinder implementations like sframe.

---
 arch/Kconfig                      |  4 +++
 include/asm-generic/unwind_user.h |  9 ++++++
 include/linux/unwind_user_types.h |  1 +
 kernel/unwind/user.c              | 49 +++++++++++++++++++++++++++++--
 4 files changed, 60 insertions(+), 3 deletions(-)
 create mode 100644 include/asm-generic/unwind_user.h
arch/Kconfig +4 -0
index c6fa2b3ecbc6..cf996cbb8142 100644
@@ -438,6 +438,10 @@ config HAVE_HARDLOCKUP_DETECTOR_ARCH
 config UNWIND_USER
 	bool
 
+config HAVE_UNWIND_USER_FP
+	bool
+	select UNWIND_USER
+
 config AS_SFRAME
 	def_bool $(as-instr,.cfi_sections .sframe\n.cfi_startproc\n.cfi_endproc)
 
include/asm-generic/unwind_user.h new file +9 -0
new file mode 100644
index 000000000000..832425502fb3
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_GENERIC_UNWIND_USER_H
+#define _ASM_GENERIC_UNWIND_USER_H
+
+#ifndef ARCH_INIT_USER_FP_FRAME
+ #define ARCH_INIT_USER_FP_FRAME
+#endif
+
+#endif /* _ASM_GENERIC_UNWIND_USER_H */
include/linux/unwind_user_types.h +1 -0
index 6ed1b4ae74e1..65bd070eb6b0 100644
@@ -6,6 +6,7 @@
 
 enum unwind_user_type {
 	UNWIND_USER_TYPE_NONE,
+	UNWIND_USER_TYPE_FP,
 };
 
 struct unwind_stacktrace {
kernel/unwind/user.c +46 -3

67 lines not on this page · raw message

Signature · 1 line
2.48.1
#40 lore raw

Re: [PATCH v4 12/39] unwind_user: Add frame pointer support

Quoted 8 lines · Josh Poimboeuf

Add optional support for user space frame pointer unwinding. If supported, the arch needs to enable CONFIG_HAVE_UNWIND_USER_FP and define ARCH_INIT_USER_FP_FRAME.

By encoding the frame offsets in struct unwind_user_frame, much of this code can also be reused for future unwinder implementations like sframe.

Quoted 9 lines
---
 arch/Kconfig                      |  4 +++
 include/asm-generic/unwind_user.h |  9 ++++++
 include/linux/unwind_user_types.h |  1 +
 kernel/unwind/user.c              | 49 +++++++++++++++++++++++++++++--
 4 files changed, 60 insertions(+), 3 deletions(-)
 create mode 100644 include/asm-generic/unwind_user.h

Do you plan to reuse this logic for stack unwinding done by perf subsystem in perf_callchain_user()? See is_uprobe_at_func_entry() parts and also fixup_uretprobe_trampoline_entries() for some of the quirks that have to be taken into account when doing frame pointer-based unwinding. It would be great not to lose those in this new reimplementation.

Not sure what's the best way to avoid duplicating the logic, but I thought I'd bring that up.

Quoted 15 lines
arch/Kconfig +4 -0
index c6fa2b3ecbc6..cf996cbb8142 100644
@@ -438,6 +438,10 @@ config HAVE_HARDLOCKUP_DETECTOR_ARCH
 config UNWIND_USER
        bool
 
+config HAVE_UNWIND_USER_FP
+       bool
+       select UNWIND_USER
+
 config AS_SFRAME
        def_bool $(as-instr,.cfi_sections .sframe\n.cfi_startproc\n.cfi_endproc)
 

[...]

author maintainer
#41 lore raw

Re: [PATCH v4 12/39] unwind_user: Add frame pointer support

Quoted 26 lines · Andrii Nakryiko
Quoted 8 lines · Josh Poimboeuf

Add optional support for user space frame pointer unwinding. If supported, the arch needs to enable CONFIG_HAVE_UNWIND_USER_FP and define ARCH_INIT_USER_FP_FRAME.

By encoding the frame offsets in struct unwind_user_frame, much of this code can also be reused for future unwinder implementations like sframe.

Quoted 9 lines
---
 arch/Kconfig                      |  4 +++
 include/asm-generic/unwind_user.h |  9 ++++++
 include/linux/unwind_user_types.h |  1 +
 kernel/unwind/user.c              | 49 +++++++++++++++++++++++++++++--
 4 files changed, 60 insertions(+), 3 deletions(-)
 create mode 100644 include/asm-generic/unwind_user.h

Do you plan to reuse this logic for stack unwinding done by perf subsystem in perf_callchain_user()? See is_uprobe_at_func_entry() parts and also fixup_uretprobe_trampoline_entries() for some of the quirks that have to be taken into account when doing frame pointer-based unwinding. It would be great not to lose those in this new reimplementation.

Quoted 2 lines

Not sure what's the best way to avoid duplicating the logic, but I thought I'd bring that up.

Indeed! That was on the todo list and somehow evaporated.

Signature · 1 line
Josh
maintainer
#42 lore raw

Re: [PATCH v4 12/39] unwind_user: Add frame pointer support

Quoted 11 lines · On Fri, 24 Jan 2025 10:16:10 -0800 Josh Poimboeuf <[email protected]> wrote:
Quoted 9 lines

Do you plan to reuse this logic for stack unwinding done by perf subsystem in perf_callchain_user()? See is_uprobe_at_func_entry() parts and also fixup_uretprobe_trampoline_entries() for some of the quirks that have to be taken into account when doing frame pointer-based unwinding. It would be great not to lose those in this new reimplementation.

Not sure what's the best way to avoid duplicating the logic, but I thought I'd bring that up.

Indeed! That was on the todo list and somehow evaporated.

I'm getting ready to post an update of these patches, but I want to mention that this has not been addressed, and I'm replying here to make sure it stays on the radar.

-- Steve

author maintainer
#43 lore raw

PATCH v4 13/39 unwind_user/x86: Enable frame pointer unwinding on x86

In reply to Josh Poimboeuf

Use ARCH_INIT_USER_FP_FRAME to describe how frame pointers are unwound on x86, and enable CONFIG_HAVE_UNWIND_USER_FP accordingly so the unwind_user interfaces can be used.

---
 arch/x86/Kconfig                   |  1 +
 arch/x86/include/asm/unwind_user.h | 11 +++++++++++
 2 files changed, 12 insertions(+)
 create mode 100644 arch/x86/include/asm/unwind_user.h
arch/x86/Kconfig +1 -0
index ef6cfea9df73..f938b957a927 100644
@@ -291,6 +291,7 @@ config X86
 	select HAVE_SYSCALL_TRACEPOINTS
 	select HAVE_UACCESS_VALIDATION		if HAVE_OBJTOOL
 	select HAVE_UNSTABLE_SCHED_CLOCK
+	select HAVE_UNWIND_USER_FP		if X86_64
 	select HAVE_USER_RETURN_NOTIFIER
 	select HAVE_GENERIC_VDSO
 	select VDSO_GETRANDOM			if X86_64
arch/x86/include/asm/unwind_user.h new file +11 -0
new file mode 100644
index 000000000000..8597857bf896
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_X86_UNWIND_USER_H
+#define _ASM_X86_UNWIND_USER_H
+
+#define ARCH_INIT_USER_FP_FRAME							\
+	.cfa_off	= (s32)sizeof(long) *  2,				\
+	.ra_off		= (s32)sizeof(long) * -1,				\
+	.fp_off		= (s32)sizeof(long) * -2,				\
+	.use_fp		= true,
+
+#endif /* _ASM_X86_UNWIND_USER_H */
Signature · 1 line
2.48.1
author maintainer
#44 lore raw

PATCH v4 14/39 perf/x86: Rename get_segment_base() and make it global

In reply to Josh Poimboeuf

get_segment_base() will be used by the unwind_user code, so make it global and rename it so it doesn't conflict with a KVM function of the same name.

---
 arch/x86/events/core.c            | 10 +++++-----
 arch/x86/include/asm/perf_event.h |  2 ++
 2 files changed, 7 insertions(+), 5 deletions(-)
arch/x86/events/core.c +5 -5
index c75c482d4c52..23ac6343cf86 100644
@@ -2790,7 +2790,7 @@ valid_user_frame(const void __user *fp, unsigned long size)
 	return __access_ok(fp, size);
 }
 
-static unsigned long get_segment_base(unsigned int segment)
+unsigned long segment_base_address(unsigned int segment)
 {
 	struct desc_struct *desc;
 	unsigned int idx = segment >> 3;
@@ -2874,8 +2874,8 @@ perf_callchain_user32(struct pt_regs *regs, struct perf_callchain_entry_ctx *ent
 	if (user_64bit_mode(regs))
 		return 0;
 
-	cs_base = get_segment_base(regs->cs);
-	ss_base = get_segment_base(regs->ss);
+	cs_base = segment_base_address(regs->cs);
+	ss_base = segment_base_address(regs->ss);
 
 	fp = compat_ptr(ss_base + regs->bp);
 	pagefault_disable();
@@ -2994,11 +2994,11 @@ static unsigned long code_segment_base(struct pt_regs *regs)
 		return 0x10 * regs->cs;
 
 	if (user_mode(regs) && regs->cs != __USER_CS)
-		return get_segment_base(regs->cs);
+		return segment_base_address(regs->cs);
 #else
 	if (user_mode(regs) && !user_64bit_mode(regs) &&
 	    regs->cs != __USER32_CS)
-		return get_segment_base(regs->cs);
+		return segment_base_address(regs->cs);
 #endif
 	return 0;
 }
arch/x86/include/asm/perf_event.h +2 -0
index d95f902acc52..75956c68356f 100644
@@ -639,4 +639,6 @@ static __always_inline void perf_lopwr_cb(bool lopwr_in)
 
 #define arch_perf_out_copy_user copy_from_user_nmi
 
+unsigned long segment_base_address(unsigned int segment);
+
 #endif /* _ASM_X86_PERF_EVENT_H */
Signature · 1 line
2.48.1
maintainer
#45 lore raw

Re: [PATCH v4 14/39] perf/x86: Rename get_segment_base() and make it global

Quoted 3 lines · Josh Poimboeuf

get_segment_base() will be used by the unwind_user code, so make it global and rename it so it doesn't conflict with a KVM function of the same name.

Should it not also get moved out of the perf code in this case?

author maintainer
#46 lore raw

Re: [PATCH v4 14/39] perf/x86: Rename get_segment_base() and make it global

Quoted 6 lines · Peter Zijlstra
Quoted 3 lines · Josh Poimboeuf

get_segment_base() will be used by the unwind_user code, so make it global and rename it so it doesn't conflict with a KVM function of the same name.

Should it not also get moved out of the perf code in this case?

True, I'll try to find it a better home.

Signature · 1 line
Josh
maintainer
#47 lore raw

Re: [PATCH v4 14/39] perf/x86: Rename get_segment_base() and make it global

In reply to Josh Poimboeuf

arch/x86/events/core.c valid_user_frame(const void __user *fp, unsigned long size) · line 2796

Quoted 13 lines · On Tue, 21 Jan 2025 18:31:06 -0800 Josh Poimboeuf <[email protected]> wrote:
arch/x86/events/core.c +1 -1
index c75c482d4c52..23ac6343cf86 100644
@@ -2790,7 +2790,7 @@ valid_user_frame(const void __user *fp, unsigned long size)
 	return __access_ok(fp, size);
 }
 
-static unsigned long get_segment_base(unsigned int segment)
+unsigned long segment_base_address(unsigned int segment)
 {
 	struct desc_struct *desc;
 	unsigned int idx = segment >> 3;

As this requires interrupts disabled, and if you do move this out of this file, you probably should add:

	lockdep_assert_irqs_disabled();

-- Steve

author maintainer
#48 lore raw

Re: [PATCH v4 14/39] perf/x86: Rename get_segment_base() and make it global

Quoted 20 lines · Steven Rostedt

arch/x86/events/core.c valid_user_frame(const void __user *fp, unsigned long size) · line 2796

Quoted 13 lines · On Tue, 21 Jan 2025 18:31:06 -0800 Josh Poimboeuf <[email protected]> wrote:
arch/x86/events/core.c +1 -1
index c75c482d4c52..23ac6343cf86 100644
@@ -2790,7 +2790,7 @@ valid_user_frame(const void __user *fp, unsigned long size)
 	return __access_ok(fp, size);
 }
 
-static unsigned long get_segment_base(unsigned int segment)
+unsigned long segment_base_address(unsigned int segment)
 {
 	struct desc_struct *desc;
 	unsigned int idx = segment >> 3;

As this requires interrupts disabled, and if you do move this out of this file, you probably should add:

Quoted 1 line
	lockdep_assert_irqs_disabled();

Indeed...

Signature · 1 line
Josh
author maintainer
#49 lore raw

PATCH v4 15/39 unwind_user: Add compat mode frame pointer support

In reply to Josh Poimboeuf

Add optional support for user space compat mode frame pointer unwinding. If supported, the arch needs to enable CONFIG_HAVE_UNWIND_USER_COMPAT_FP and define ARCH_INIT_USER_COMPAT_FP_FRAME.

---
 arch/Kconfig                            |  4 +++
 include/asm-generic/Kbuild              |  2 ++
 include/asm-generic/unwind_user.h       | 15 +++++++++++
 include/asm-generic/unwind_user_types.h |  9 +++++++
 include/linux/unwind_user_types.h       |  3 +++
 kernel/unwind/user.c                    | 36 ++++++++++++++++++++++---
 6 files changed, 65 insertions(+), 4 deletions(-)
 create mode 100644 include/asm-generic/unwind_user_types.h
arch/Kconfig +4 -0
index cf996cbb8142..f1f7a3857c97 100644
@@ -442,6 +442,10 @@ config HAVE_UNWIND_USER_FP
 	bool
 	select UNWIND_USER
 
+config HAVE_UNWIND_USER_COMPAT_FP
+	bool
+	depends on HAVE_UNWIND_USER_FP
+
 config AS_SFRAME
 	def_bool $(as-instr,.cfi_sections .sframe\n.cfi_startproc\n.cfi_endproc)
 
include/asm-generic/Kbuild +2 -0
index 1b43c3a77012..2f3e4e2d8610 100644
@@ -58,6 +58,8 @@ mandatory-y += tlbflush.h
 mandatory-y += topology.h
 mandatory-y += trace_clock.h
 mandatory-y += uaccess.h
+mandatory-y += unwind_user.h
+mandatory-y += unwind_user_types.h
 mandatory-y += vermagic.h
 mandatory-y += vga.h
 mandatory-y += video.h
include/asm-generic/unwind_user.h +15 -0
index 832425502fb3..385638ce4aec 100644
@@ -2,8 +2,23 @@
 #ifndef _ASM_GENERIC_UNWIND_USER_H
 #define _ASM_GENERIC_UNWIND_USER_H
 
+#include <asm/unwind_user_types.h>
+
 #ifndef ARCH_INIT_USER_FP_FRAME
  #define ARCH_INIT_USER_FP_FRAME
 #endif
 
+#ifndef ARCH_INIT_USER_COMPAT_FP_FRAME
+ #define ARCH_INIT_USER_COMPAT_FP_FRAME
+ #define in_compat_mode(regs) false
+#endif
+
+#ifndef arch_unwind_user_init
+static inline void arch_unwind_user_init(struct unwind_user_state *state, struct pt_regs *reg) {}
+#endif
+
+#ifndef arch_unwind_user_next
+static inline void arch_unwind_user_next(struct unwind_user_state *state) {}
+#endif
+
 #endif /* _ASM_GENERIC_UNWIND_USER_H */
include/asm-generic/unwind_user_types.h new file +9 -0
new file mode 100644
index 000000000000..ee803de7c998
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_GENERIC_UNWIND_USER_TYPES_H
+#define _ASM_GENERIC_UNWIND_USER_TYPES_H
+
+#ifndef arch_unwind_user_state
+struct arch_unwind_user_state {};
+#endif
+
+#endif /* _ASM_GENERIC_UNWIND_USER_TYPES_H */
include/linux/unwind_user_types.h +3 -0
index 65bd070eb6b0..3ec4a097a3dd 100644
@@ -3,10 +3,12 @@
 #define _LINUX_UNWIND_USER_TYPES_H
 
 #include <linux/types.h>
+#include <asm/unwind_user_types.h>
 
 enum unwind_user_type {
 	UNWIND_USER_TYPE_NONE,
 	UNWIND_USER_TYPE_FP,
+	UNWIND_USER_TYPE_COMPAT_FP,
 };
 
 struct unwind_stacktrace {
@@ -25,6 +27,7 @@ struct unwind_user_state {
 	unsigned long ip;
 	unsigned long sp;
 	unsigned long fp;
+	struct arch_unwind_user_state arch;
 	enum unwind_user_type type;
 	bool done;
 };
kernel/unwind/user.c +32 -4
index 73fd4e150dfd..92963f129c6a 100644
@@ -13,12 +13,32 @@ static struct unwind_user_frame fp_frame = {
 	ARCH_INIT_USER_FP_FRAME
 };
 
+static struct unwind_user_frame compat_fp_frame = {
+	ARCH_INIT_USER_COMPAT_FP_FRAME
+};
+
 static inline bool fp_state(struct unwind_user_state *state)
 {
 	return IS_ENABLED(CONFIG_HAVE_UNWIND_USER_FP) &&
 	       state->type == UNWIND_USER_TYPE_FP;
 }
 
+static inline bool compat_state(struct unwind_user_state *state)
+{
+	return IS_ENABLED(CONFIG_HAVE_UNWIND_USER_COMPAT_FP) &&
+	       state->type == UNWIND_USER_TYPE_COMPAT_FP;
+}
+
+#define UNWIND_GET_USER_LONG(to, from, state)				\
+({									\
+	int __ret;							\
+	if (compat_state(state))					\
+		__ret = get_user(to, (u32 __user *)(from));		\
+	else								\
+		__ret = get_user(to, (u64 __user *)(from));		\
+	__ret;								\
+})
+
 int unwind_user_next(struct unwind_user_state *state)
 {
 	struct unwind_user_frame _frame;
@@ -28,7 +48,9 @@ int unwind_user_next(struct unwind_user_state *state)
 	if (state->done)
 		return -EINVAL;
 
-	if (fp_state(state))
+	if (compat_state(state))
+		frame = &compat_fp_frame;
+	else if (fp_state(state))
 		frame = &fp_frame;
 	else
 		goto the_end;
@@ -39,10 +61,10 @@ int unwind_user_next(struct unwind_user_state *state)
 	if (cfa <= state->sp)
 		goto the_end;
 
-	if (get_user(ra, (unsigned long *)(cfa + frame->ra_off)))
+	if (UNWIND_GET_USER_LONG(ra, cfa + frame->ra_off, state))
 		goto the_end;
 
-	if (frame->fp_off && get_user(fp, (unsigned long __user *)(cfa + frame->fp_off)))
+	if (frame->fp_off && UNWIND_GET_USER_LONG(fp, cfa + frame->fp_off, state))
 		goto the_end;
 
 	state->ip = ra;
@@ -50,6 +72,8 @@ int unwind_user_next(struct unwind_user_state *state)
 	if (frame->fp_off)
 		state->fp = fp;
 
+	arch_unwind_user_next(state);
+
 	return 0;
 
 the_end:
@@ -68,7 +92,9 @@ int unwind_user_start(struct unwind_user_state *state)
 		return -EINVAL;
 	}
 
-	if (IS_ENABLED(CONFIG_HAVE_UNWIND_USER_FP))
+	if (IS_ENABLED(CONFIG_HAVE_UNWIND_USER_COMPAT_FP) && in_compat_mode(regs))
+		state->type = UNWIND_USER_TYPE_COMPAT_FP;
+	else if (IS_ENABLED(CONFIG_HAVE_UNWIND_USER_FP))
 		state->type = UNWIND_USER_TYPE_FP;
 	else
 		state->type = UNWIND_USER_TYPE_NONE;
@@ -77,6 +103,8 @@ int unwind_user_start(struct unwind_user_state *state)
 	state->sp = user_stack_pointer(regs);
 	state->fp = frame_pointer(regs);
 
+	arch_unwind_user_init(state, regs);
+
 	return 0;
 }
 
Signature · 1 line
2.48.1
author maintainer
#50 lore raw

PATCH v4 16/39 unwind_user/x86: Enable compat mode frame pointer unwinding on x86

In reply to Josh Poimboeuf

Use ARCH_INIT_USER_COMPAT_FP_FRAME to describe how frame pointers are unwound on x86, and implement the hooks needed to add the segment base addresses. Enable HAVE_UNWIND_USER_COMPAT_FP if the system has compat mode compiled in.

---
 arch/x86/Kconfig                         |  1 +
 arch/x86/include/asm/unwind_user.h       | 50 ++++++++++++++++++++++++
 arch/x86/include/asm/unwind_user_types.h | 17 ++++++++
 3 files changed, 68 insertions(+)
 create mode 100644 arch/x86/include/asm/unwind_user_types.h
arch/x86/Kconfig +1 -0
index f938b957a927..08c44db0fefb 100644
@@ -291,6 +291,7 @@ config X86
 	select HAVE_SYSCALL_TRACEPOINTS
 	select HAVE_UACCESS_VALIDATION		if HAVE_OBJTOOL
 	select HAVE_UNSTABLE_SCHED_CLOCK
+	select HAVE_UNWIND_USER_COMPAT_FP	if IA32_EMULATION
 	select HAVE_UNWIND_USER_FP		if X86_64
 	select HAVE_USER_RETURN_NOTIFIER
 	select HAVE_GENERIC_VDSO
arch/x86/include/asm/unwind_user.h +50 -0

60 lines not on this page · raw message

arch/x86/include/asm/unwind_user_types.h new file +17 -0
new file mode 100644
index 000000000000..d7074dc5f0ce
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_UNWIND_USER_TYPES_H
+#define _ASM_UNWIND_USER_TYPES_H
+
+#ifdef CONFIG_IA32_EMULATION
+
+struct arch_unwind_user_state {
+	unsigned long ss_base;
+	unsigned long cs_base;
+};
+#define arch_unwind_user_state arch_unwind_user_state
+
+#endif /* CONFIG_IA32_EMULATION */
+
+#include <asm-generic/unwind_user_types.h>
+
+#endif /* _ASM_UNWIND_USER_TYPES_H */
Signature · 1 line
2.48.1
author maintainer
#51 lore raw

PATCH v4 17/39 unwind_user/sframe: Add support for reading .sframe headers

In reply to Josh Poimboeuf

In preparation for unwinding user space stacks with sframe, add basic sframe compile infrastructure and support for reading the .sframe section header.

sframe_add_section() reads the header and unconditionally returns an error, so it's not very useful yet. A subsequent patch will improve that.

---
 arch/Kconfig           |   3 +
 include/linux/sframe.h |  36 +++++++++++
 kernel/unwind/Makefile |   3 +-
 kernel/unwind/sframe.c | 136 +++++++++++++++++++++++++++++++++++++++++
 kernel/unwind/sframe.h |  71 +++++++++++++++++++++
 5 files changed, 248 insertions(+), 1 deletion(-)
 create mode 100644 include/linux/sframe.h
 create mode 100644 kernel/unwind/sframe.c
 create mode 100644 kernel/unwind/sframe.h
arch/Kconfig +3 -0
index f1f7a3857c97..23edd0e4e16a 100644
@@ -446,6 +446,9 @@ config HAVE_UNWIND_USER_COMPAT_FP
 	bool
 	depends on HAVE_UNWIND_USER_FP
 
+config HAVE_UNWIND_USER_SFRAME
+	bool
+
 config AS_SFRAME
 	def_bool $(as-instr,.cfi_sections .sframe\n.cfi_startproc\n.cfi_endproc)
 
include/linux/sframe.h new file +36 -0
new file mode 100644
index 000000000000..3bfaf21869c2
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_SFRAME_H
+#define _LINUX_SFRAME_H
+
+#include <linux/mm_types.h>
+#include <linux/unwind_user_types.h>
+
+#ifdef CONFIG_HAVE_UNWIND_USER_SFRAME
+
+struct sframe_section {
+	unsigned long	sframe_start;
+	unsigned long	sframe_end;
+	unsigned long	text_start;
+	unsigned long	text_end;
+
+	unsigned long	fdes_start;
+	unsigned long	fres_start;
+	unsigned long	fres_end;
+	unsigned int	num_fdes;
+
+	signed char	ra_off;
+	signed char	fp_off;
+};
+
+extern int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end,
+			      unsigned long text_start, unsigned long text_end);
+extern int sframe_remove_section(unsigned long sframe_addr);
+
+#else /* !CONFIG_HAVE_UNWIND_USER_SFRAME */
+
+static inline int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end, unsigned long text_start, unsigned long text_end) { return -ENOSYS; }
+static inline int sframe_remove_section(unsigned long sframe_addr) { return -ENOSYS; }
+
+#endif /* CONFIG_HAVE_UNWIND_USER_SFRAME */
+
+#endif /* _LINUX_SFRAME_H */
kernel/unwind/Makefile +2 -1
index 349ce3677526..f70380d7a6a6 100644
@@ -1 +1,2 @@
- obj-$(CONFIG_UNWIND_USER) += user.o
+ obj-$(CONFIG_UNWIND_USER)		+= user.o
+ obj-$(CONFIG_HAVE_UNWIND_USER_SFRAME)	+= sframe.o
kernel/unwind/sframe.c new file +136 -0

136 lines not on this page · raw message

kernel/unwind/sframe.h new file +71 -0

71 lines not on this page · raw message

Signature · 1 line
2.48.1
#52 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 21 lines · Josh Poimboeuf

In preparation for unwinding user space stacks with sframe, add basic sframe compile infrastructure and support for reading the .sframe section header.

sframe_add_section() reads the header and unconditionally returns an error, so it's not very useful yet. A subsequent patch will improve that.

---
 arch/Kconfig           |   3 +
 include/linux/sframe.h |  36 +++++++++++
 kernel/unwind/Makefile |   3 +-
 kernel/unwind/sframe.c | 136 +++++++++++++++++++++++++++++++++++++++++
 kernel/unwind/sframe.h |  71 +++++++++++++++++++++
 5 files changed, 248 insertions(+), 1 deletion(-)
 create mode 100644 include/linux/sframe.h
 create mode 100644 kernel/unwind/sframe.c
 create mode 100644 kernel/unwind/sframe.h

[...]

include/linux/sframe.h line 31

Quoted 8 lines
include/linux/sframe.h new file +8 -0
+
+extern int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end,
+                             unsigned long text_start, unsigned long text_end);
+extern int sframe_remove_section(unsigned long sframe_addr);
+
+#else /* !CONFIG_HAVE_UNWIND_USER_SFRAME */
+
+static inline int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end, unsigned long text_start, unsigned long text_end) { return -ENOSYS; }

nit: very-very long, wrap it?

Quoted 5 lines
include/linux/sframe.h new file +5 -0
+static inline int sframe_remove_section(unsigned long sframe_addr) { return -ENOSYS; }
+
+#endif /* CONFIG_HAVE_UNWIND_USER_SFRAME */
+
+#endif /* _LINUX_SFRAME_H */

[...]

Quoted 4 lines
kernel/unwind/sframe.c new file +4 -0
+static int sframe_read_header(struct sframe_section *sec)
+{
+       unsigned long header_end, fdes_start, fdes_end, fres_start, fres_end;
+       struct sframe_header shdr;

kernel/unwind/sframe.c line 40

Quoted 10 lines
kernel/unwind/sframe.c new file +10 -0
+       unsigned int num_fdes;
+
+       if (copy_from_user(&shdr, (void __user *)sec->sframe_start, sizeof(shdr))) {
+               dbg("header usercopy failed\n");
+               return -EFAULT;
+       }
+
+       if (shdr.preamble.magic != SFRAME_MAGIC ||
+           shdr.preamble.version != SFRAME_VERSION_2 ||
+           !(shdr.preamble.flags & SFRAME_F_FDE_SORTED) ||

probably more a question to Indu, but why is this sorting not mandatory and part of SFrame "standard"? How realistically non-sorted FDEs would work in practice? Ain't nobody got time to sort them just to unwind the stack...

kernel/unwind/sframe.c line 46

Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+           shdr.auxhdr_len) {
+               dbg("bad/unsupported sframe header\n");
+               return -EINVAL;
+       }
+
+       if (!shdr.num_fdes || !shdr.num_fres) {

given SFRAME_F_FRAME_POINTER in the header, is it really that nonsensical and illegal to have zero FDEs/FREs? Maybe we should allow that?

kernel/unwind/sframe.c line 52

Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+               dbg("no fde/fre entries\n");
+               return -EINVAL;
+       }
+
+       header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
+       if (header_end >= sec->sframe_end) {

if we allow zero FDEs/FREs, header_end == sec->sframe_end is legal, right?

kernel/unwind/sframe.c line 63

Quoted 11 lines
kernel/unwind/sframe.c new file +11 -0
+               dbg("header doesn't fit in section\n");
+               return -EINVAL;
+       }
+
+       num_fdes   = shdr.num_fdes;
+       fdes_start = header_end + shdr.fdes_off;
+       fdes_end   = fdes_start + (num_fdes * sizeof(struct sframe_fde));
+
+       fres_start = header_end + shdr.fres_off;
+       fres_end   = fres_start + shdr.fre_len;
+

maybe use check_add_overflow() in all the above calculation, at least on 32-bit arches this all can overflow and it's not clear if below sanity check detects all possible overflows

Quoted 16 lines
kernel/unwind/sframe.c new file +16 -0
+       if (fres_start < fdes_end || fres_end > sec->sframe_end) {
+               dbg("inconsistent fde/fre offsets\n");
+               return -EINVAL;
+       }
+
+       sec->num_fdes           = num_fdes;
+       sec->fdes_start         = fdes_start;
+       sec->fres_start         = fres_start;
+       sec->fres_end           = fres_end;
+
+       sec->ra_off             = shdr.cfa_fixed_ra_offset;
+       sec->fp_off             = shdr.cfa_fixed_fp_offset;
+
+       return 0;
+}
+

[...]

Quoted 53 lines
kernel/unwind/sframe.h new file +47 -0
new file mode 100644
index 000000000000..e9bfccfaf5b4
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * From https://www.sourceware.org/binutils/docs/sframe-spec.html
+ */
+#ifndef _SFRAME_H
+#define _SFRAME_H
+
+#include <linux/types.h>
+
+#define SFRAME_VERSION_1                       1
+#define SFRAME_VERSION_2                       2
+#define SFRAME_MAGIC                           0xdee2
+
+#define SFRAME_F_FDE_SORTED                    0x1
+#define SFRAME_F_FRAME_POINTER                 0x2
+
+#define SFRAME_ABI_AARCH64_ENDIAN_BIG          1
+#define SFRAME_ABI_AARCH64_ENDIAN_LITTLE       2
+#define SFRAME_ABI_AMD64_ENDIAN_LITTLE         3
+
+#define SFRAME_FDE_TYPE_PCINC                  0
+#define SFRAME_FDE_TYPE_PCMASK                 1
+
+struct sframe_preamble {
+       u16     magic;
+       u8      version;
+       u8      flags;
+} __packed;
+
+struct sframe_header {
+       struct sframe_preamble preamble;
+       u8      abi_arch;
+       s8      cfa_fixed_fp_offset;
+       s8      cfa_fixed_ra_offset;
+       u8      auxhdr_len;
+       u32     num_fdes;
+       u32     num_fres;
+       u32     fre_len;
+       u32     fdes_off;
+       u32     fres_off;
+} __packed;
+
+#define SFRAME_HEADER_SIZE(header) \
+       ((sizeof(struct sframe_header) + header.auxhdr_len))
+
+#define SFRAME_AARCH64_PAUTH_KEY_A             0
+#define SFRAME_AARCH64_PAUTH_KEY_B             1

kernel/unwind/sframe.h line 57

Quoted 10 lines
kernel/unwind/sframe.h new file +10 -0
+
+struct sframe_fde {
+       s32     start_addr;
+       u32     func_size;
+       u32     fres_off;
+       u32     fres_num;
+       u8      info;
+       u8      rep_size;
+       u16 padding;
+} __packed;

I couldn't understand from SFrame itself, but why do sframe_header, sframe_preamble, and sframe_fde have to be marked __packed, if it's all naturally aligned (intentionally and by design)?..

Quoted 17 lines
kernel/unwind/sframe.h new file +14 -1
+
+#define SFRAME_FUNC_FRE_TYPE(data)             (data & 0xf)
+#define SFRAME_FUNC_FDE_TYPE(data)             ((data >> 4) & 0x1)
+#define SFRAME_FUNC_PAUTH_KEY(data)            ((data >> 5) & 0x1)
+
+#define SFRAME_BASE_REG_FP                     0
+#define SFRAME_BASE_REG_SP                     1
+
+#define SFRAME_FRE_CFA_BASE_REG_ID(data)       (data & 0x1)
+#define SFRAME_FRE_OFFSET_COUNT(data)          ((data >> 1) & 0xf)
+#define SFRAME_FRE_OFFSET_SIZE(data)           ((data >> 5) & 0x3)
+#define SFRAME_FRE_MANGLED_RA_P(data)          ((data >> 7) & 0x1)
+
+#endif /* _SFRAME_H */
--

2.48.1

author maintainer
#53 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 4 lines · Andrii Nakryiko
Quoted 1 line · Josh Poimboeuf

+static inline int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end, unsigned long text_start, unsigned long text_end) { return -ENOSYS; }

nit: very-very long, wrap it?

That was intentional as it's just an empty stub, but yeah, maybe 160 chars is a bit much.

Quoted 8 lines

kernel/unwind/sframe.c line 40

Quoted 3 lines
kernel/unwind/sframe.c new file +3 -0
+       if (shdr.preamble.magic != SFRAME_MAGIC ||
+           shdr.preamble.version != SFRAME_VERSION_2 ||
+           !(shdr.preamble.flags & SFRAME_F_FDE_SORTED) ||

probably more a question to Indu, but why is this sorting not mandatory and part of SFrame "standard"? How realistically non-sorted FDEs would work in practice? Ain't nobody got time to sort them just to unwind the stack...

No idea...

Quoted 5 lines
Quoted 1 line

+ if (!shdr.num_fdes || !shdr.num_fres) {

given SFRAME_F_FRAME_POINTER in the header, is it really that nonsensical and illegal to have zero FDEs/FREs? Maybe we should allow that?

It would seem a bit silly to create an empty .sframe section just to set that SFRAME_F_FRAME_POINTER bit. Regardless, there's nothing the kernel can do with that.

Quoted 8 lines

kernel/unwind/sframe.c line 52

Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+               dbg("no fde/fre entries\n");
+               return -EINVAL;
+       }
+
+       header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
+       if (header_end >= sec->sframe_end) {

if we allow zero FDEs/FREs, header_end == sec->sframe_end is legal, right?

I suppose so, but again I'm not seeing any reason to support that.

Quoted 7 lines
Quoted 7 lines
kernel/unwind/sframe.c new file +7 -0
+               dbg("header doesn't fit in section\n");
+               return -EINVAL;
+       }
+
+       num_fdes   = shdr.num_fdes;
+       fdes_start = header_end + shdr.fdes_off;
+       fdes_end   = fdes_start + (num_fdes * sizeof(struct sframe_fde));
Quoted 8 lines

kernel/unwind/sframe.c line 63

Quoted 4 lines
kernel/unwind/sframe.c new file +4 -0
+
+       fres_start = header_end + shdr.fres_off;
+       fres_end   = fres_start + shdr.fre_len;
+

maybe use check_add_overflow() in all the above calculation, at least on 32-bit arches this all can overflow and it's not clear if below sanity check detects all possible overflows

Ok, I'll look into it.

Quoted 24 lines
Quoted 24 lines
kernel/unwind/sframe.h new file +24 -0
+struct sframe_preamble {
+       u16     magic;
+       u8      version;
+       u8      flags;
+} __packed;
+
+struct sframe_header {
+       struct sframe_preamble preamble;
+       u8      abi_arch;
+       s8      cfa_fixed_fp_offset;
+       s8      cfa_fixed_ra_offset;
+       u8      auxhdr_len;
+       u32     num_fdes;
+       u32     num_fres;
+       u32     fre_len;
+       u32     fdes_off;
+       u32     fres_off;
+} __packed;
+
+struct sframe_fde {
+       s32     start_addr;
+       u32     func_size;
+       u32     fres_off;
+       u32     fres_num;
Quoted 8 lines

kernel/unwind/sframe.h line 57

Quoted 4 lines
kernel/unwind/sframe.h new file +4 -0
+       u8      info;
+       u8      rep_size;
+       u16 padding;
+} __packed;

I couldn't understand from SFrame itself, but why do sframe_header, sframe_preamble, and sframe_fde have to be marked __packed, if it's all naturally aligned (intentionally and by design)?..

Right, but the spec says they're all packed. Maybe the point is that some future sframe version is free to introduce unaligned fields.

Signature · 1 line
Josh
maintainer
#54 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 15 lines · On Fri, 24 Jan 2025 11:21:59 -0800 Josh Poimboeuf <[email protected]> wrote:
Quoted 3 lines

given SFRAME_F_FRAME_POINTER in the header, is it really that nonsensical and illegal to have zero FDEs/FREs? Maybe we should allow that?

It would seem a bit silly to create an empty .sframe section just to set that SFRAME_F_FRAME_POINTER bit. Regardless, there's nothing the kernel can do with that.

Quoted 7 lines
Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+               dbg("no fde/fre entries\n");
+               return -EINVAL;
+       }
+
+       header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
+       if (header_end >= sec->sframe_end) {  
Quoted 3 lines
Quoted 1 line

if we allow zero FDEs/FREs, header_end == sec->sframe_end is legal, right?

I suppose so, but again I'm not seeing any reason to support that.

Hmm, could that be useful for implementing a way to dynamically grow or shrink an sframe because of jits? I'm just thinking about placeholders or something.

-- Steve

author maintainer
#55 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 18 lines · Steven Rostedt
Quoted 15 lines · On Fri, 24 Jan 2025 11:21:59 -0800 Josh Poimboeuf <[email protected]> wrote:
Quoted 3 lines

given SFRAME_F_FRAME_POINTER in the header, is it really that nonsensical and illegal to have zero FDEs/FREs? Maybe we should allow that?

It would seem a bit silly to create an empty .sframe section just to set that SFRAME_F_FRAME_POINTER bit. Regardless, there's nothing the kernel can do with that.

Quoted 7 lines
Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+               dbg("no fde/fre entries\n");
+               return -EINVAL;
+       }
+
+       header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
+       if (header_end >= sec->sframe_end) {  
Quoted 7 lines
Quoted 3 lines
Quoted 1 line

if we allow zero FDEs/FREs, header_end == sec->sframe_end is legal, right?

I suppose so, but again I'm not seeing any reason to support that.

Hmm, could that be useful for implementing a way to dynamically grow or shrink an sframe because of jits? I'm just thinking about placeholders or sohething.

Maybe?

I was thinking the kernel would have sframe_section placeholders for JIT code sections, so when sframe_find() retrieves the struct for a given IP, it sees the JIT flag is set along with a pointer to the in-memory shared "sframe section", then goes to read that to get the corresponding sframe entry (insert erratic hand waving).

It's still early days but it's quite possible the in-memory "sframe section" formats might end up looking pretty different from the .sframe file section spec.

Signature · 1 line
Josh
#56 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

In reply to Josh Poimboeuf
Quoted 22 lines · Josh Poimboeuf
Quoted 4 lines · Andrii Nakryiko
Quoted 1 line · Josh Poimboeuf

+static inline int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end, unsigned long text_start, unsigned long text_end) { return -ENOSYS; }

nit: very-very long, wrap it?

That was intentional as it's just an empty stub, but yeah, maybe 160 chars is a bit much.

Quoted 8 lines

kernel/unwind/sframe.c line 40

Quoted 3 lines
kernel/unwind/sframe.c new file +3 -0
+       if (shdr.preamble.magic != SFRAME_MAGIC ||
+           shdr.preamble.version != SFRAME_VERSION_2 ||
+           !(shdr.preamble.flags & SFRAME_F_FDE_SORTED) ||

probably more a question to Indu, but why is this sorting not mandatory and part of SFrame "standard"? How realistically non-sorted FDEs would work in practice? Ain't nobody got time to sort them just to unwind the stack...

No idea...

Quoted 2 lines
Quoted 1 line

+ if (!shdr.num_fdes || !shdr.num_fres) {

Quoted 8 lines
Quoted 3 lines

given SFRAME_F_FRAME_POINTER in the header, is it really that nonsensical and illegal to have zero FDEs/FREs? Maybe we should allow that?

It would seem a bit silly to create an empty .sframe section just to set that SFRAME_F_FRAME_POINTER bit. Regardless, there's nothing the kernel can do with that.

Yes, in theory, it is allowed (as per the specification) to have an SFrame section with zero number of FDEs/FREs. But since such a section will not be useful, I share the opinion that it makes sense to disallow it in the current unwinding contexts, for now (JIT usecase may change things later).

SFRAME_F_FRAME_POINTER flag is not being set currently by GAS/GNU ld at all.

Quoted 62 lines
Quoted 8 lines

kernel/unwind/sframe.c line 52

Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+               dbg("no fde/fre entries\n");
+               return -EINVAL;
+       }
+
+       header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
+       if (header_end >= sec->sframe_end) {

if we allow zero FDEs/FREs, header_end == sec->sframe_end is legal, right?

I suppose so, but again I'm not seeing any reason to support that.

Quoted 7 lines
Quoted 7 lines
kernel/unwind/sframe.c new file +7 -0
+               dbg("header doesn't fit in section\n");
+               return -EINVAL;
+       }
+
+       num_fdes   = shdr.num_fdes;
+       fdes_start = header_end + shdr.fdes_off;
+       fdes_end   = fdes_start + (num_fdes * sizeof(struct sframe_fde));
Quoted 8 lines

kernel/unwind/sframe.c line 63

Quoted 4 lines
kernel/unwind/sframe.c new file +4 -0
+
+       fres_start = header_end + shdr.fres_off;
+       fres_end   = fres_start + shdr.fre_len;
+

maybe use check_add_overflow() in all the above calculation, at least on 32-bit arches this all can overflow and it's not clear if below sanity check detects all possible overflows

Ok, I'll look into it.

Quoted 24 lines
Quoted 24 lines
kernel/unwind/sframe.h new file +24 -0
+struct sframe_preamble {
+       u16     magic;
+       u8      version;
+       u8      flags;
+} __packed;
+
+struct sframe_header {
+       struct sframe_preamble preamble;
+       u8      abi_arch;
+       s8      cfa_fixed_fp_offset;
+       s8      cfa_fixed_ra_offset;
+       u8      auxhdr_len;
+       u32     num_fdes;
+       u32     num_fres;
+       u32     fre_len;
+       u32     fdes_off;
+       u32     fres_off;
+} __packed;
+
+struct sframe_fde {
+       s32     start_addr;
+       u32     func_size;
+       u32     fres_off;
+       u32     fres_num;
Quoted 8 lines

kernel/unwind/sframe.h line 57

Quoted 4 lines
kernel/unwind/sframe.h new file +4 -0
+       u8      info;
+       u8      rep_size;
+       u16 padding;
+} __packed;

I couldn't understand from SFrame itself, but why do sframe_header, sframe_preamble, and sframe_fde have to be marked __packed, if it's all naturally aligned (intentionally and by design)?..

Quoted 3 lines

Right, but the spec says they're all packed. Maybe the point is that some future sframe version is free to introduce unaligned fields.

SFrame specification aims to keep SFrame header and SFrame FDE members at aligned boundaries in future versions.

Only SFrame FRE related accesses may have unaligned accesses.

#57 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 33 lines · Indu Bhagat
Quoted 22 lines · Josh Poimboeuf
Quoted 4 lines · Andrii Nakryiko
Quoted 1 line · Josh Poimboeuf

+static inline int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end, unsigned long text_start, unsigned long text_end) { return -ENOSYS; }

nit: very-very long, wrap it?

That was intentional as it's just an empty stub, but yeah, maybe 160 chars is a bit much.

Quoted 8 lines

kernel/unwind/sframe.c line 40

Quoted 3 lines
kernel/unwind/sframe.c new file +3 -0
+       if (shdr.preamble.magic != SFRAME_MAGIC ||
+           shdr.preamble.version != SFRAME_VERSION_2 ||
+           !(shdr.preamble.flags & SFRAME_F_FDE_SORTED) ||

probably more a question to Indu, but why is this sorting not mandatory and part of SFrame "standard"? How realistically non-sorted FDEs would work in practice? Ain't nobody got time to sort them just to unwind the stack...

No idea...

Quoted 2 lines
Quoted 1 line

+ if (!shdr.num_fdes || !shdr.num_fres) {

Quoted 8 lines
Quoted 3 lines

given SFRAME_F_FRAME_POINTER in the header, is it really that nonsensical and illegal to have zero FDEs/FREs? Maybe we should allow that?

It would seem a bit silly to create an empty .sframe section just to set that SFRAME_F_FRAME_POINTER bit. Regardless, there's nothing the kernel can do with that.

Quoted 6 lines

Yes, in theory, it is allowed (as per the specification) to have an SFrame section with zero number of FDEs/FREs. But since such a section will not be useful, I share the opinion that it makes sense to disallow it in the current unwinding contexts, for now (JIT usecase may change things later).

I disagree, actually. If it's a legal thing, it shouldn't be randomly rejected. If we later make use of that, we'd have to worry not to accidentally cause problems on older kernels that arbitrarily rejected empty FDE just because it didn't make sense at some point (without causing any issues).

Quoted 12 lines

SFRAME_F_FRAME_POINTER flag is not being set currently by GAS/GNU ld at all.

Quoted 10 lines
Quoted 8 lines

kernel/unwind/sframe.c line 52

Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+               dbg("no fde/fre entries\n");
+               return -EINVAL;
+       }
+
+       header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
+       if (header_end >= sec->sframe_end) {

if we allow zero FDEs/FREs, header_end == sec->sframe_end is legal, right?

I suppose so, but again I'm not seeing any reason to support that.

Let's invert this. Is there any reason why it shouldn't be supported? ;)

Quoted 52 lines
Quoted 52 lines
Quoted 16 lines

kernel/unwind/sframe.c line 63

Quoted 12 lines
kernel/unwind/sframe.c new file +11 -0
+               dbg("header doesn't fit in section\n");
+               return -EINVAL;
+       }
+
+       num_fdes   = shdr.num_fdes;
+       fdes_start = header_end + shdr.fdes_off;
+       fdes_end   = fdes_start + (num_fdes * sizeof(struct sframe_fde));
+
+       fres_start = header_end + shdr.fres_off;
+       fres_end   = fres_start + shdr.fre_len;
+

maybe use check_add_overflow() in all the above calculation, at least on 32-bit arches this all can overflow and it's not clear if below sanity check detects all possible overflows

Ok, I'll look into it.

Quoted 32 lines
Quoted 18 lines
kernel/unwind/sframe.h new file +18 -0
+struct sframe_preamble {
+       u16     magic;
+       u8      version;
+       u8      flags;
+} __packed;
+
+struct sframe_header {
+       struct sframe_preamble preamble;
+       u8      abi_arch;
+       s8      cfa_fixed_fp_offset;
+       s8      cfa_fixed_ra_offset;
+       u8      auxhdr_len;
+       u32     num_fdes;
+       u32     num_fres;
+       u32     fre_len;
+       u32     fdes_off;
+       u32     fres_off;
+} __packed;

kernel/unwind/sframe.h line 57

Quoted 10 lines
kernel/unwind/sframe.h new file +10 -0
+
+struct sframe_fde {
+       s32     start_addr;
+       u32     func_size;
+       u32     fres_off;
+       u32     fres_num;
+       u8      info;
+       u8      rep_size;
+       u16 padding;
+} __packed;

I couldn't understand from SFrame itself, but why do sframe_header, sframe_preamble, and sframe_fde have to be marked __packed, if it's all naturally aligned (intentionally and by design)?..

Quoted 8 lines
Quoted 3 lines

Right, but the spec says they're all packed. Maybe the point is that some future sframe version is free to introduce unaligned fields.

SFrame specification aims to keep SFrame header and SFrame FDE members at aligned boundaries in future versions.

Only SFrame FRE related accesses may have unaligned accesses.

Yeah, and it's actually bothering me quite a lot :) I have a tentative proposal, maybe we can discuss this for SFrame v3? Let me briefly outline the idea.

So, currently in v2, FREs within FDEs use an array-of-structs layout. If we use preudo-C type definitions, it would be something like this for FDE + its FREs:

struct FDE_and_FREs {
    struct sframe_func_desc_entry fde_metadata;

    union FRE {
        struct FRE8 {
            u8 sfre_start_address;
            u8 sfre_info;
            u8|u16|u32 offsets[M];
        }
        struct FRE16 {
            u16 sfre_start_address;
            u16 sfre_info;
            u8|u16|u32 offsets[M];
        }
        struct FRE32 {
            u32 sfre_start_address;
            u32 sfre_info;
            u8|u16|u32 offsets[M];
        }
    } fres[N] __packed;
};

where all fres[i]s are one of those FRE8/FRE16/FRE32, so start addresses have the same size, but each FRE has potentially different offsets sizing, so there is no common alignment, and so everything has to be packed and unaligned.

But what if we take a struct-of-arrays approach and represent it more like:

struct FDE_and_FREs {
    struct sframe_func_desc_entry fde_metadata;
    u8|u16|u32 start_addrs[N]; /* can extend to u64 as well */
    u8 sfre_infos[N];
    u8 offsets8[M8];
    u16 offsets16[M16] __aligned(2);
    u32 offsets32[M32] __aligned(4);
    /* we can naturally extend to support also u64 offsets */
};

i.e., we split all FRE records into their three constituents: start addresses, info bytes, and then each FRE can fall into either 8-, 16-, or 32-bit offsets "bucket". We collect all the offsets, depending on their size, into these aligned offsets{8,16,32} arrays (with natural extension to 64 bits, if necessary), with at most wasting 1-3 bytes to ensure proper alignment everywhere.

Note, at this point we need to decide if we want to make FREs binary searchable or not.

If not, we don't really need anything extra. As we process each start_addrs[i] and sfre_infos[i] to find matching FRE, we keep track of how many 8-, 16-, and 32-bit offsets already processed FREs consumed, and when we find the right one, we know exactly the starting index within offset{8,16,32}. Done.

But if we were to make FREs binary searchable, we need to basically have an index of offset pointers to quickly find offsetsX[j] position corresponding to FRE #i. For that, we can have an extra array right next to start_addrs, "semantically parallel" to it:

u8|u16|u32 start_addrs[N];
u8|u16|u32 offset_idxs[N];

where start_addrs[i] corresponds to offset_idxs[i], and offset_idxs[i] points to the first offset corresponding to FRE #i in offsetX[] array (depending on FRE's "bitness"). This is a bit more storage for this offset index, but for FDEs with lots of FREs this might be a worthwhile tradeoff.

Few points:
a) we can decide this "binary searchability" per-FDE, and for FDEs
with 1-2-3 FREs not bother, while those with more FREs would be searchable ones with index. So we can combine both fast lookups, natural alignment of on-disk format, and compactness. The presence of index is just another bit in FDE metadata.
b) bitness of offset_idxs[] can be coupled with bitness of
start_addrs (for simplicity), or could be completely independent and identified by FDE's metadata (2 more bits to define this just like start_addr bitness is defined). Independent probably would be my preference, with linker (or whoever will be producing .sframe data) can pick the smallest bitness that is sufficient to represent everything.

Yes, it's a bit more complicated to draw and explain, but everything will be nicely aligned, extensible to 64 bits, and (optionally at least) binary searchable. Implementation-wise on the kernel side it shouldn't be significantly more involved. Maybe the compiler would need to be a bit smarter when producing FDE data, but it's no rocket science.

Thoughts?

author maintainer
#58 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 7 lines · Andrii Nakryiko
Quoted 6 lines

Yes, in theory, it is allowed (as per the specification) to have an SFrame section with zero number of FDEs/FREs. But since such a section will not be useful, I share the opinion that it makes sense to disallow it in the current unwinding contexts, for now (JIT usecase may change things later).

Quoted 5 lines

I disagree, actually. If it's a legal thing, it shouldn't be randomly rejected. If we later make use of that, we'd have to worry not to accidentally cause problems on older kernels that arbitrarily rejected empty FDE just because it didn't make sense at some point (without causing any issues).

If such older kernels don't do anything with the section anyway, what's the point of pretending they do?

Returning an error would actually make more sense as it communicates that the kernel doesn't support whatever hypothetical thing you're trying to do with 0 FDEs.

Quoted 9 lines
Quoted 9 lines

SFRAME_F_FRAME_POINTER flag is not being set currently by GAS/GNU ld at all.

Quoted 7 lines
Quoted 7 lines
Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+               dbg("no fde/fre entries\n");
+               return -EINVAL;
+       }
+
+       header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
+       if (header_end >= sec->sframe_end) {
Quoted 5 lines
Quoted 3 lines
Quoted 3 lines
Quoted 1 line

if we allow zero FDEs/FREs, header_end == sec->sframe_end is legal, right?

I suppose so, but again I'm not seeing any reason to support that.

Let's invert this. Is there any reason why it shouldn't be supported? ;)

It's simple, we don't add code to "support" some vague hypothetical.

For whatever definition of "support", since there's literally nothing the kernel can do with that.

Quoted 12 lines

But what if we take a struct-of-arrays approach and represent it more like:

struct FDE_and_FREs {
    struct sframe_func_desc_entry fde_metadata;
    u8|u16|u32 start_addrs[N]; /* can extend to u64 as well */
    u8 sfre_infos[N];
    u8 offsets8[M8];
    u16 offsets16[M16] __aligned(2);
    u32 offsets32[M32] __aligned(4);
    /* we can naturally extend to support also u64 offsets */
};
Quoted 6 lines

i.e., we split all FRE records into their three constituents: start addresses, info bytes, and then each FRE can fall into either 8-, 16-, or 32-bit offsets "bucket". We collect all the offsets, depending on their size, into these aligned offsets{8,16,32} arrays (with natural extension to 64 bits, if necessary), with at most wasting 1-3 bytes to ensure proper alignment everywhere.

Makes sense. Though I also have another idea below.

Quoted 9 lines

Note, at this point we need to decide if we want to make FREs binary searchable or not.

If not, we don't really need anything extra. As we process each start_addrs[i] and sfre_infos[i] to find matching FRE, we keep track of how many 8-, 16-, and 32-bit offsets already processed FREs consumed, and when we find the right one, we know exactly the starting index within offset{8,16,32}. Done.

Quoted 7 lines

But if we were to make FREs binary searchable, we need to basically have an index of offset pointers to quickly find offsetsX[j] position corresponding to FRE #i. For that, we can have an extra array right next to start_addrs, "semantically parallel" to it:

u8|u16|u32 start_addrs[N];
u8|u16|u32 offset_idxs[N];

Binary search would definitely help. I did a crude histogram for "FREs per FDE" for a few binaries on my test system:

gdb (the biggest binary on my test system):

10th Percentile: 1
20th Percentile: 1
30th Percentile: 1
40th Percentile: 3
50th Percentile: 5
60th Percentile: 8
70th Percentile: 11
80th Percentile: 14
90th Percentile: 16
100th Percentile: 472

bash:

10th Percentile: 1
20th Percentile: 1
30th Percentile: 3
40th Percentile: 5
50th Percentile: 7
60th Percentile: 9
70th Percentile: 12
80th Percentile: 16
90th Percentile: 17
100th Percentile: 46

glibc:

10th Percentile: 1
20th Percentile: 1
30th Percentile: 1
40th Percentile: 1
50th Percentile: 4
60th Percentile: 6
70th Percentile: 9
80th Percentile: 14
90th Percentile: 16
100th Percentile: 44

libpython:

10th Percentile: 1
20th Percentile: 3
30th Percentile: 4
40th Percentile: 6
50th Percentile: 8
60th Percentile: 11
70th Percentile: 12
80th Percentile: 16
90th Percentile: 20
100th Percentile: 112

So binary search would help in a lot of cases.

However, if we're going that route, we might want to even consider a completely revamped data layout. For example:

One insight is that the vast majority of (cfa, fp, ra) tuples aren't unique. They could be deduped by storing the unique tuples in a standalone 'fre_data' array which is referenced by another address-specific array.

  struct fre_data {
	s8|s16|s32 cfa, fp, ra;
	u8 info;

};
struct fre_data fre_data[num_fre_data];

The storage sizes of cfa/fp/ra can be a constant specified in the global sframe header. It's fine all being the same size as it looks like this array wouldn't typically be more than a few hundred entries anyway.

Then there would be an array of sorted FRE entries which reference the fre_data[] entries:

struct fre {

  	s32|s64 start_address;
	u8|u16 fre_data_idx;
  } __packed;
  struct fre fres[num_fres];

(For alignment reasons that should probably be two separate arrays, even though not ideal for cache locality)

Here again the field storage sizes would be specified in the global sframe header.

Note FDEs aren't even needed here as the unwinder doesn't need to know when a function begins/ends. The only info needed by the unwinder is just the fre_data struct. So a simple binary search of fres[] is all that's really needed.

But wait, there's more...

The binary search could be made significantly faster using a small fast lookup array which is indexed evenly across the text address offset space, similar to what ORC does:

  u32 fre_chunks[num_chunks];

The text address space (starting at offset 0) can be split into 'num_chunks' chunks of size 'chunk_size'. The value of fre_chunks[offset/chunk_size] is an index into the fres[] array.

Taking my gdb binary as an example:

.text is 6417058 bytes, with 146997 total sframe FREs. Assuming a chunk size of 1024, fre_chunks[] needs 6417058/1024 = 6267 entries.

For unwinding at text offset 0x400000, the index into fre_chunks[] would be 0x400000/1024 = 4096. If fre_chunks[4096] == 96074 and fre_chunks[4096+1] == 96098, you need only do a binary search of the 24 entries between fres[96074] && fres[96098] rather than searching the entire 146997 byte array.

.sframe size calculation:

    374 unique fre_data entries (out of 146997 total FREs!)
    = 374 * (2 * 3) = 2244 bytes

    146997 fre entries
    = 146997 * (4 + 2) = 881982 bytes

    .text size 6417058 (chunk_size = 1024, num_chunks=6267)
    = 6267 * 8 = 43869 bytes

Approximate total .sframe size would be 2244 + 881982 + 8192 = 906k, plus negligible header size. Which is smaller than the v2 .sframe on my gdb binary (985k).

With the chunked lookup table, the avg lookup is:

log2(146997/6267) = ~4.5 iterations

whereas a full binary search would be:

log2(146997) = 17 iterations

So assuming I got all that math right, it's over 3x faster and the binary is smaller (or at least should be roughly comparable).

Of course the downside is it's an all new format. Presumably the linker would need to do more work than it's currently doing, e.g., find all the duplicates and set up the data accordingly.

Signature · 1 line
Josh
#59 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 33 lines · Josh Poimboeuf
Quoted 7 lines · Andrii Nakryiko
Quoted 6 lines

Yes, in theory, it is allowed (as per the specification) to have an SFrame section with zero number of FDEs/FREs. But since such a section will not be useful, I share the opinion that it makes sense to disallow it in the current unwinding contexts, for now (JIT usecase may change things later).

Quoted 5 lines

I disagree, actually. If it's a legal thing, it shouldn't be randomly rejected. If we later make use of that, we'd have to worry not to accidentally cause problems on older kernels that arbitrarily rejected empty FDE just because it didn't make sense at some point (without causing any issues).

If such older kernels don't do anything with the section anyway, what's the point of pretending they do?

Returning an error would actually make more sense as it communicates that the kernel doesn't support whatever hypothetical thing you're trying to do with 0 FDEs.

Quoted 11 lines
Quoted 11 lines

SFRAME_F_FRAME_POINTER flag is not being set currently by GAS/GNU ld at all.

Quoted 9 lines
Quoted 8 lines

kernel/unwind/sframe.c line 52

Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+               dbg("no fde/fre entries\n");
+               return -EINVAL;
+       }
+
+       header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
+       if (header_end >= sec->sframe_end) {

if we allow zero FDEs/FREs, header_end == sec->sframe_end is legal, right?

Quoted 8 lines
Quoted 3 lines
Quoted 1 line
Quoted 1 line

I suppose so, but again I'm not seeing any reason to support that.

Let's invert this. Is there any reason why it shouldn't be supported? ;)

It's simple, we don't add code to "support" some vague hypothetical.

For whatever definition of "support", since there's literally nothing the kernel can do with that.

If under the format specification it's legal, there is no reason for the kernel to proactively reject it, IMO. But ok, whatever.

Quoted 94 lines
Quoted 13 lines

But what if we take a struct-of-arrays approach and represent it more like:

struct FDE_and_FREs {
    struct sframe_func_desc_entry fde_metadata;
    u8|u16|u32 start_addrs[N]; /* can extend to u64 as well */
    u8 sfre_infos[N];
    u8 offsets8[M8];
    u16 offsets16[M16] __aligned(2);
    u32 offsets32[M32] __aligned(4);
    /* we can naturally extend to support also u64 offsets */
};
Quoted 6 lines

i.e., we split all FRE records into their three constituents: start addresses, info bytes, and then each FRE can fall into either 8-, 16-, or 32-bit offsets "bucket". We collect all the offsets, depending on their size, into these aligned offsets{8,16,32} arrays (with natural extension to 64 bits, if necessary), with at most wasting 1-3 bytes to ensure proper alignment everywhere.

Makes sense. Though I also have another idea below.

Quoted 9 lines

Note, at this point we need to decide if we want to make FREs binary searchable or not.

If not, we don't really need anything extra. As we process each start_addrs[i] and sfre_infos[i] to find matching FRE, we keep track of how many 8-, 16-, and 32-bit offsets already processed FREs consumed, and when we find the right one, we know exactly the starting index within offset{8,16,32}. Done.

Quoted 7 lines

But if we were to make FREs binary searchable, we need to basically have an index of offset pointers to quickly find offsetsX[j] position corresponding to FRE #i. For that, we can have an extra array right next to start_addrs, "semantically parallel" to it:

u8|u16|u32 start_addrs[N];
u8|u16|u32 offset_idxs[N];

Binary search would definitely help. I did a crude histogram for "FREs per FDE" for a few binaries on my test system:

gdb (the biggest binary on my test system):

10th Percentile: 1
20th Percentile: 1
30th Percentile: 1
40th Percentile: 3
50th Percentile: 5
60th Percentile: 8
70th Percentile: 11
80th Percentile: 14
90th Percentile: 16
100th Percentile: 472

bash:

10th Percentile: 1
20th Percentile: 1
30th Percentile: 3
40th Percentile: 5
50th Percentile: 7
60th Percentile: 9
70th Percentile: 12
80th Percentile: 16
90th Percentile: 17
100th Percentile: 46

glibc:

10th Percentile: 1
20th Percentile: 1
30th Percentile: 1
40th Percentile: 1
50th Percentile: 4
60th Percentile: 6
70th Percentile: 9
80th Percentile: 14
90th Percentile: 16
100th Percentile: 44

libpython:

10th Percentile: 1
20th Percentile: 3
30th Percentile: 4
40th Percentile: 6
50th Percentile: 8
60th Percentile: 11
70th Percentile: 12
80th Percentile: 16
90th Percentile: 20
100th Percentile: 112

Quoted 1 line

So binary search would help in a lot of cases.

yep, agreed, seems like a worthwhile thing to support, given the stats (I suspect big production C++ applications might be even worse in this regard)

Quoted 19 lines

However, if we're going that route, we might want to even consider a completely revamped data layout. For example:

One insight is that the vast majority of (cfa, fp, ra) tuples aren't unique. They could be deduped by storing the unique tuples in a standalone 'fre_data' array which is referenced by another address-specific array.

  struct fre_data {
        s8|s16|s32 cfa, fp, ra;
        u8 info;
  };
  struct fre_data fre_data[num_fre_data];

The storage sizes of cfa/fp/ra can be a constant specified in the global sframe header. It's fine all being the same size as it looks like this array wouldn't typically be more than a few hundred entries anyway.

Quoted 6 lines

Then there would be an array of sorted FRE entries which reference the fre_data[] entries:

  struct fre {
        s32|s64 start_address;
        u8|u16 fre_data_idx;

even u16 seems dangerous, I'd use u32, not sure it's worth limiting the format just to 64K unique combinations

Quoted 55 lines
  } __packed;
  struct fre fres[num_fres];

(For alignment reasons that should probably be two separate arrays, even though not ideal for cache locality)

Here again the field storage sizes would be specified in the global sframe header.

Note FDEs aren't even needed here as the unwinder doesn't need to know when a function begins/ends. The only info needed by the unwinder is just the fre_data struct. So a simple binary search of fres[] is all that's really needed.

But wait, there's more...

The binary search could be made significantly faster using a small fast lookup array which is indexed evenly across the text address offset space, similar to what ORC does:

  u32 fre_chunks[num_chunks];

The text address space (starting at offset 0) can be split into 'num_chunks' chunks of size 'chunk_size'. The value of fre_chunks[offset/chunk_size] is an index into the fres[] array.

Taking my gdb binary as an example:

.text is 6417058 bytes, with 146997 total sframe FREs. Assuming a chunk size of 1024, fre_chunks[] needs 6417058/1024 = 6267 entries.

For unwinding at text offset 0x400000, the index into fre_chunks[] would be 0x400000/1024 = 4096. If fre_chunks[4096] == 96074 and fre_chunks[4096+1] == 96098, you need only do a binary search of the 24 entries between fres[96074] && fres[96098] rather than searching the entire 146997 byte array.

.sframe size calculation:

    374 unique fre_data entries (out of 146997 total FREs!)
    = 374 * (2 * 3) = 2244 bytes

    146997 fre entries
    = 146997 * (4 + 2) = 881982 bytes

    .text size 6417058 (chunk_size = 1024, num_chunks=6267)
    = 6267 * 8 = 43869 bytes

Approximate total .sframe size would be 2244 + 881982 + 8192 = 906k, plus negligible header size. Which is smaller than the v2 .sframe on my gdb binary (985k).

With the chunked lookup table, the avg lookup is:

Quoted 8 lines

log2(146997/6267) = ~4.5 iterations

whereas a full binary search would be:

log2(146997) = 17 iterations

So assuming I got all that math right, it's over 3x faster and the binary is smaller (or at least should be roughly comparable).

I'm not sure about this chunked lookup approach for arbitrary user space applications. Those executable sections can be a) big and b) discontiguous. E.g., one of the production binaries I looked at. Here are its three main executable sections:

...
[17] .bolt.org.text PROGBITS 000000000b00e640 0ae0d640

       0000000011ad621c  0000000000000000  AX       0     0     64
...
  [48] .text             PROGBITS         000000001e600000  1ce00000
       0000000000775dd8  0000000000000000  AX       0     0     2097152
  [49] .text.cold        PROGBITS         000000001ed75e00  1d575e00
       00000000007d3271  0000000000000000  AX       0     0     64

...

Total text size is about 300MB:

Quoted 1 line
Quoted 1 line
Quoted 1 line

0x0000000000775dd8 + 0x00000000007d3271 + 0x0000000011ad621c

312603237

Section #17 ends at:

Quoted 1 line
Quoted 1 line
Quoted 1 line

hex(0x0000000011ad621c + 0x000000000b00e640)

'0x1cae485c'

While .text starts at 000000001e600000, so we have a gap of ~28MB:

Quoted 1 line
Quoted 1 line
Quoted 1 line

0x000000001e600000 - 0x1cae485c

28424100

So unless we do something more clever to support multiple discontiguous chunks, this seems like a bad fit for user space.

I think having all this just binary searchable is already a big win anyways and should be plenty fast, no?

Quoted 4 lines

Of course the downside is it's an all new format. Presumably the linker would need to do more work than it's currently doing, e.g., find all the duplicates and set up the data accordingly.

I do like the idea of deduplicating those records and just indexing them, as in practice this should probably be much more compact. So it might be worth looking into this some more. I'll defer to Indu.

Quoted 3 lines

--
Josh

author maintainer
#60 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 25 lines · Andrii Nakryiko

On Tue, Jan 28, 2025 at 6:02 PM Josh Poimboeuf <[email protected]> wrote:
I'm not sure about this chunked lookup approach for arbitrary user space applications. Those executable sections can be a) big and b) discontiguous. E.g., one of the production binaries I looked at. Here are its three main executable sections:

...
[17] .bolt.org.text PROGBITS 000000000b00e640 0ae0d640

       0000000011ad621c  0000000000000000  AX       0     0     64
...
  [48] .text             PROGBITS         000000001e600000  1ce00000
       0000000000775dd8  0000000000000000  AX       0     0     2097152
  [49] .text.cold        PROGBITS         000000001ed75e00  1d575e00
       00000000007d3271  0000000000000000  AX       0     0     64

...

Total text size is about 300MB:

Quoted 1 line
Quoted 1 line
Quoted 1 line

0x0000000000775dd8 + 0x00000000007d3271 + 0x0000000011ad621c

312603237

Section #17 ends at:

Quoted 1 line
Quoted 1 line
Quoted 1 line

hex(0x0000000011ad621c + 0x000000000b00e640)

'0x1cae485c'

Quoted 7 lines

While .text starts at 000000001e600000, so we have a gap of ~28MB:

Quoted 1 line
Quoted 1 line
Quoted 1 line

0x000000001e600000 - 0x1cae485c

28424100

So unless we do something more clever to support multiple discontiguous chunks, this seems like a bad fit for user space.

Nothing clever needed, we could just have multiple sframe sections, each one with a pointer to its text segment. That would also have the benefit of allowing the sframe data to be much more compact for the noncontiguous cases.

Quoted 2 lines

I think having all this just binary searchable is already a big win anyways and should be plenty fast, no?

Sframe is trying to compete with frame pointers which are MUCH faster. 3-4x faster in my testing, not including the page faults (which tend to only affect performance in the very beginning).

Signature · 1 line
Josh
#61 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

In reply to Josh Poimboeuf
Quoted 134 lines · Josh Poimboeuf
Quoted 7 lines · Andrii Nakryiko
Quoted 6 lines

Yes, in theory, it is allowed (as per the specification) to have an SFrame section with zero number of FDEs/FREs. But since such a section will not be useful, I share the opinion that it makes sense to disallow it in the current unwinding contexts, for now (JIT usecase may change things later).

Quoted 5 lines

I disagree, actually. If it's a legal thing, it shouldn't be randomly rejected. If we later make use of that, we'd have to worry not to accidentally cause problems on older kernels that arbitrarily rejected empty FDE just because it didn't make sense at some point (without causing any issues).

If such older kernels don't do anything with the section anyway, what's the point of pretending they do?

Returning an error would actually make more sense as it communicates that the kernel doesn't support whatever hypothetical thing you're trying to do with 0 FDEs.

Quoted 9 lines
Quoted 9 lines

SFRAME_F_FRAME_POINTER flag is not being set currently by GAS/GNU ld at all.

Quoted 7 lines
Quoted 7 lines
Quoted 6 lines
kernel/unwind/sframe.c new file +6 -0
+               dbg("no fde/fre entries\n");
+               return -EINVAL;
+       }
+
+       header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
+       if (header_end >= sec->sframe_end) {
Quoted 5 lines
Quoted 3 lines
Quoted 3 lines
Quoted 1 line

if we allow zero FDEs/FREs, header_end == sec->sframe_end is legal, right?

I suppose so, but again I'm not seeing any reason to support that.

Let's invert this. Is there any reason why it shouldn't be supported? ;)

It's simple, we don't add code to "support" some vague hypothetical.

For whatever definition of "support", since there's literally nothing the kernel can do with that.

Quoted 12 lines

But what if we take a struct-of-arrays approach and represent it more like:

struct FDE_and_FREs {
     struct sframe_func_desc_entry fde_metadata;
     u8|u16|u32 start_addrs[N]; /* can extend to u64 as well */
     u8 sfre_infos[N];
     u8 offsets8[M8];
     u16 offsets16[M16] __aligned(2);
     u32 offsets32[M32] __aligned(4);
     /* we can naturally extend to support also u64 offsets */
};
Quoted 6 lines

i.e., we split all FRE records into their three constituents: start addresses, info bytes, and then each FRE can fall into either 8-, 16-, or 32-bit offsets "bucket". We collect all the offsets, depending on their size, into these aligned offsets{8,16,32} arrays (with natural extension to 64 bits, if necessary), with at most wasting 1-3 bytes to ensure proper alignment everywhere.

Makes sense. Though I also have another idea below.

Quoted 9 lines

Note, at this point we need to decide if we want to make FREs binary searchable or not.

If not, we don't really need anything extra. As we process each start_addrs[i] and sfre_infos[i] to find matching FRE, we keep track of how many 8-, 16-, and 32-bit offsets already processed FREs consumed, and when we find the right one, we know exactly the starting index within offset{8,16,32}. Done.

Quoted 7 lines

But if we were to make FREs binary searchable, we need to basically have an index of offset pointers to quickly find offsetsX[j] position corresponding to FRE #i. For that, we can have an extra array right next to start_addrs, "semantically parallel" to it:

u8|u16|u32 start_addrs[N];
u8|u16|u32 offset_idxs[N];

Binary search would definitely help. I did a crude histogram for "FREs per FDE" for a few binaries on my test system:

gdb (the biggest binary on my test system):

10th Percentile: 1
20th Percentile: 1
30th Percentile: 1
40th Percentile: 3
50th Percentile: 5
60th Percentile: 8
70th Percentile: 11
80th Percentile: 14
90th Percentile: 16
100th Percentile: 472

bash:

10th Percentile: 1
20th Percentile: 1
30th Percentile: 3
40th Percentile: 5
50th Percentile: 7
60th Percentile: 9
70th Percentile: 12
80th Percentile: 16
90th Percentile: 17
100th Percentile: 46

glibc:

10th Percentile: 1
20th Percentile: 1
30th Percentile: 1
40th Percentile: 1
50th Percentile: 4
60th Percentile: 6
70th Percentile: 9
80th Percentile: 14
90th Percentile: 16
100th Percentile: 44

libpython:

10th Percentile: 1
20th Percentile: 3
30th Percentile: 4
40th Percentile: 6
50th Percentile: 8
60th Percentile: 11
70th Percentile: 12
80th Percentile: 16
90th Percentile: 20
100th Percentile: 112

Quoted 2 lines

So binary search would help in a lot of cases.

Thanks for gathering this.

I suspect on aarch64, the numbers will be very different (leaning towards very low number of FREs per FDE).

Making SFrame FREs amenable to binary search can be targeted. Both your and Andrii's proposal do address that...

Quoted 8 lines

However, if we're going that route, we might want to even consider a completely revamped data layout. For example:

One insight is that the vast majority of (cfa, fp, ra) tuples aren't unique. They could be deduped by storing the unique tuples in a standalone 'fre_data' array which is referenced by another address-specific array.

Quoted 6 lines
   struct fre_data {
	s8|s16|s32 cfa, fp, ra;
	u8 info;
   };
   struct fre_data fre_data[num_fre_data];

We had the same observation at the time of SFrame V1. And this method of compaction (deduped tuples) was brain-stormed a bit. Back then, the costs were thought to be:
- more work at build time.
- an additional data access once the FRE is found (as there is
indirection).

So it was really compaction at the costs above. We did steer towards simplicity and the SFrame FRE is what it stands today.

The difference in the pros and cons now from then:
- pros: helps mitigate unaligned accesses
- cons: interferes slightly with the design goal of efficient
addition and removal of stack trace information per function for JIT. Think "removal" as the set of actions necessary for addressing fragmentation in SFrame section data in JIT usecase.

Quoted 17 lines

The storage sizes of cfa/fp/ra can be a constant specified in the global sframe header. It's fine all being the same size as it looks like this array wouldn't typically be more than a few hundred entries anyway.

Then there would be an array of sorted FRE entries which reference the fre_data[] entries:

   struct fre {
   	s32|s64 start_address;
	u8|u16 fre_data_idx;
	
   } __packed;
   struct fre fres[num_fres];

(For alignment reasons that should probably be two separate arrays, even though not ideal for cache locality)

Quoted 8 lines

Here again the field storage sizes would be specified in the global sframe header.

Note FDEs aren't even needed here as the unwinder doesn't need to know when a function begins/ends. The only info needed by the unwinder is just the fre_data struct. So a simple binary search of fres[] is all that's really needed.

Splitting out information (start_address) to an FDE (as done in V1/V2) has the benefit that a job like relocating information is proportional to O(NumFunctions).

In the case above, IIUC, where the proposal puts start_address in the FRE, these costs will be (much) higher.

In addition, not being able to identify stack trace information per function will affect the JIT usecase. We need to able to mark stack trace information stale for functions in JIT environment.

I think the first conceptual landing point in the information layout should be a per-function entry.

Quoted 53 lines

But wait, there's more...

The binary search could be made significantly faster using a small fast lookup array which is indexed evenly across the text address offset space, similar to what ORC does:

   u32 fre_chunks[num_chunks];

The text address space (starting at offset 0) can be split into 'num_chunks' chunks of size 'chunk_size'. The value of fre_chunks[offset/chunk_size] is an index into the fres[] array.

Taking my gdb binary as an example:

.text is 6417058 bytes, with 146997 total sframe FREs. Assuming a chunk size of 1024, fre_chunks[] needs 6417058/1024 = 6267 entries.

For unwinding at text offset 0x400000, the index into fre_chunks[] would be 0x400000/1024 = 4096. If fre_chunks[4096] == 96074 and fre_chunks[4096+1] == 96098, you need only do a binary search of the 24 entries between fres[96074] && fres[96098] rather than searching the entire 146997 byte array.

.sframe size calculation:

     374 unique fre_data entries (out of 146997 total FREs!)
     = 374 * (2 * 3) = 2244 bytes

     146997 fre entries
     = 146997 * (4 + 2) = 881982 bytes

     .text size 6417058 (chunk_size = 1024, num_chunks=6267)
     = 6267 * 8 = 43869 bytes

Approximate total .sframe size would be 2244 + 881982 + 8192 = 906k, plus negligible header size. Which is smaller than the v2 .sframe on my gdb binary (985k).

With the chunked lookup table, the avg lookup is:

   log2(146997/6267) = ~4.5 iterations

whereas a full binary search would be:

   log2(146997) = 17 iterations

So assuming I got all that math right, it's over 3x faster and the binary is smaller (or at least should be roughly comparable).

Of course the downside is it's an all new format. Presumably the linker would need to do more work than it's currently doing, e.g., find all the duplicates and set up the data accordingly.

author maintainer
#62 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 26 lines · Indu Bhagat
Quoted 8 lines · Josh Poimboeuf

However, if we're going that route, we might want to even consider a completely revamped data layout. For example:

One insight is that the vast majority of (cfa, fp, ra) tuples aren't unique. They could be deduped by storing the unique tuples in a standalone 'fre_data' array which is referenced by another address-specific array.

Quoted 6 lines
   struct fre_data {
	s8|s16|s32 cfa, fp, ra;
	u8 info;
   };
   struct fre_data fre_data[num_fre_data];

We had the same observation at the time of SFrame V1. And this method of compaction (deduped tuples) was brain-stormed a bit. Back then, the costs were thought to be:
- more work at build time.
- an additional data access once the FRE is found (as there is
indirection).

So it was really compaction at the costs above. We did steer towards simplicity and the SFrame FRE is what it stands today.

Quoted 6 lines

The difference in the pros and cons now from then:
- pros: helps mitigate unaligned accesses
- cons: interferes slightly with the design goal of efficient addition and
removal of stack trace information per function for JIT. Think "removal" as the set of actions necessary for addressing fragmentation in SFrame section data in JIT usecase.

If fre_data[] is allowed to have duplicates then the deduping could be optional.

Quoted 11 lines
Quoted 4 lines

Note FDEs aren't even needed here as the unwinder doesn't need to know when a function begins/ends. The only info needed by the unwinder is just the fre_data struct. So a simple binary search of fres[] is all that's really needed.

Splitting out information (start_address) to an FDE (as done in V1/V2) has the benefit that a job like relocating information is proportional to O(NumFunctions).

In the case above, IIUC, where the proposal puts start_address in the FRE, these costs will be (much) higher.

I'm not sure I follow, is this referring to the link-time work of sorting things?

Quoted 3 lines

In addition, not being able to identify stack trace information per function will affect the JIT usecase. We need to able to mark stack trace information stale for functions in JIT environment.

Maybe, though it's hard to really say how any of these changes would affect JIT without knowing what those interfaces are going to look like.

Signature · 1 line
Josh
#63 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 46 lines · Josh Poimboeuf
Quoted 26 lines · Indu Bhagat
Quoted 8 lines · Josh Poimboeuf

However, if we're going that route, we might want to even consider a completely revamped data layout. For example:

One insight is that the vast majority of (cfa, fp, ra) tuples aren't unique. They could be deduped by storing the unique tuples in a standalone 'fre_data' array which is referenced by another address-specific array.

Quoted 6 lines
    struct fre_data {
	s8|s16|s32 cfa, fp, ra;
	u8 info;
    };
    struct fre_data fre_data[num_fre_data];

We had the same observation at the time of SFrame V1. And this method of compaction (deduped tuples) was brain-stormed a bit. Back then, the costs were thought to be:
- more work at build time.
- an additional data access once the FRE is found (as there is
indirection).

So it was really compaction at the costs above. We did steer towards simplicity and the SFrame FRE is what it stands today.

Quoted 6 lines

The difference in the pros and cons now from then:
- pros: helps mitigate unaligned accesses
- cons: interferes slightly with the design goal of efficient addition and
removal of stack trace information per function for JIT. Think "removal" as the set of actions necessary for addressing fragmentation in SFrame section data in JIT usecase.

If fre_data[] is allowed to have duplicates then the deduping could be optional.

Quoted 9 lines
Quoted 4 lines

Note FDEs aren't even needed here as the unwinder doesn't need to know when a function begins/ends. The only info needed by the unwinder is just the fre_data struct. So a simple binary search of fres[] is all that's really needed.

Splitting out information (start_address) to an FDE (as done in V1/V2) has the benefit that a job like relocating information is proportional to O(NumFunctions).

Quoted 6 lines
Quoted 2 lines

In the case above, IIUC, where the proposal puts start_address in the FRE, these costs will be (much) higher.

I'm not sure I follow, is this referring to the link-time work of sorting things?

I meant the work of tracking the start address of each function. This could be done at link-time as is done in most cases.

But also depending on the case : e.g., kernel module loader will need to apply these relocations in the .rela.sframe section...

If the granularity is finer than a function, more number of relocations will need to be applied.

Quoted 7 lines
Quoted 3 lines

In addition, not being able to identify stack trace information per function will affect the JIT usecase. We need to able to mark stack trace information stale for functions in JIT environment.

Maybe, though it's hard to really say how any of these changes would affect JIT without knowing what those interfaces are going to look like.

#64 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

In reply to Josh Poimboeuf
Quoted 4 lines · Josh Poimboeuf

Note FDEs aren't even needed here as the unwinder doesn't need to know when a function begins/ends. The only info needed by the unwinder is just the fre_data struct. So a simple binary search of fres[] is all that's really needed.

In SFrame V2 FDEs specify ranges bound by function start address and length. FREs in contrast specify open ranges bounded by start address. Their effect ends either with the next FRE becoming into effect or when their FDE range ends.
This concept enables holes in the .text section which do not have any valid FDE/FRE information associated.

Your proposal lacks some sort of mechanism to replicate those holes. It could be FDEs with a flag (or no offsets?) that specifies their range has no valid information.

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#65 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 14 lines · Jens Remus
Quoted 4 lines · Josh Poimboeuf

Note FDEs aren't even needed here as the unwinder doesn't need to know when a function begins/ends. The only info needed by the unwinder is just the fre_data struct. So a simple binary search of fres[] is all that's really needed.

In SFrame V2 FDEs specify ranges bound by function start address and length. FREs in contrast specify open ranges bounded by start address. Their effect ends either with the next FRE becoming into effect or when their FDE range ends.
This concept enables holes in the .text section which do not have any valid FDE/FRE information associated.

Quoted 3 lines

Your proposal lacks some sort of mechanism to replicate those holes. It could be FDEs with a flag (or no offsets?) that specifies their range has no valid information.

In ORC, a hole is simply specified by an ORC entry (aka "FRE") of type UNDEFINED.

That could be done here as well: the linker would replace a gap with an "undefined" FRE which could be identified either by setting a bit in the FRE header, or by using a special fre_data[] entry. For example, fre_data index 0 could just be a placeholder for the undefined FRE type.

Signature · 1 line
Josh
#66 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

In reply to Andrii Nakryiko
Quoted 34 lines · Andrii Nakryiko
Quoted 34 lines
Quoted 34 lines
Quoted 23 lines
Quoted 23 lines
kernel/unwind/sframe.h new file +23 -0
+struct sframe_preamble {
+       u16     magic;
+       u8      version;
+       u8      flags;
+} __packed;
+
+struct sframe_header {
+       struct sframe_preamble preamble;
+       u8      abi_arch;
+       s8      cfa_fixed_fp_offset;
+       s8      cfa_fixed_ra_offset;
+       u8      auxhdr_len;
+       u32     num_fdes;
+       u32     num_fres;
+       u32     fre_len;
+       u32     fdes_off;
+       u32     fres_off;
+} __packed;
+
+struct sframe_fde {
+       s32     start_addr;
+       u32     func_size;
+       u32     fres_off;
Quoted 8 lines

kernel/unwind/sframe.h line 57

Quoted 5 lines
kernel/unwind/sframe.h new file +5 -0
+       u32     fres_num;
+       u8      info;
+       u8      rep_size;
+       u16 padding;
+} __packed;

I couldn't understand from SFrame itself, but why do sframe_header, sframe_preamble, and sframe_fde have to be marked __packed, if it's all naturally aligned (intentionally and by design)?..

Right, but the spec says they're all packed. Maybe the point is that some future sframe version is free to introduce unaligned fields.

Quoted 8 lines
Quoted 4 lines

SFrame specification aims to keep SFrame header and SFrame FDE members at aligned boundaries in future versions.

Only SFrame FRE related accesses may have unaligned accesses.

Yeah, and it's actually bothering me quite a lot 🙂 I have a tentative proposal, maybe we can discuss this for SFrame v3? Let me briefly outline the idea.

I looked at the idea below. It could work wrt unaligned accesses.

Speaking of unaligned accesses, I will ask away: Is the reason to avoid unaligned accesses performance hit or are there other practical reasons to it ?

Quoted 73 lines

So, currently in v2, FREs within FDEs use an array-of-structs layout. If we use preudo-C type definitions, it would be something like this for FDE + its FREs:

struct FDE_and_FREs {
     struct sframe_func_desc_entry fde_metadata;

     union FRE {
         struct FRE8 {
             u8 sfre_start_address;
             u8 sfre_info;
             u8|u16|u32 offsets[M];
         }
         struct FRE16 {
             u16 sfre_start_address;
             u16 sfre_info;
             u8|u16|u32 offsets[M];
         }
         struct FRE32 {
             u32 sfre_start_address;
             u32 sfre_info;
             u8|u16|u32 offsets[M];
         }
     } fres[N] __packed;
};

where all fres[i]s are one of those FRE8/FRE16/FRE32, so start addresses have the same size, but each FRE has potentially different offsets sizing, so there is no common alignment, and so everything has to be packed and unaligned.

But what if we take a struct-of-arrays approach and represent it more like:

struct FDE_and_FREs {
     struct sframe_func_desc_entry fde_metadata;
     u8|u16|u32 start_addrs[N]; /* can extend to u64 as well */
     u8 sfre_infos[N];
     u8 offsets8[M8];
     u16 offsets16[M16] __aligned(2);
     u32 offsets32[M32] __aligned(4);
     /* we can naturally extend to support also u64 offsets */
};

i.e., we split all FRE records into their three constituents: start addresses, info bytes, and then each FRE can fall into either 8-, 16-, or 32-bit offsets "bucket". We collect all the offsets, depending on their size, into these aligned offsets{8,16,32} arrays (with natural extension to 64 bits, if necessary), with at most wasting 1-3 bytes to ensure proper alignment everywhere.

Note, at this point we need to decide if we want to make FREs binary searchable or not.

If not, we don't really need anything extra. As we process each start_addrs[i] and sfre_infos[i] to find matching FRE, we keep track of how many 8-, 16-, and 32-bit offsets already processed FREs consumed, and when we find the right one, we know exactly the starting index within offset{8,16,32}. Done.

But if we were to make FREs binary searchable, we need to basically have an index of offset pointers to quickly find offsetsX[j] position corresponding to FRE #i. For that, we can have an extra array right next to start_addrs, "semantically parallel" to it:

u8|u16|u32 start_addrs[N];
u8|u16|u32 offset_idxs[N];

where start_addrs[i] corresponds to offset_idxs[i], and offset_idxs[i] points to the first offset corresponding to FRE #i in offsetX[] array (depending on FRE's "bitness"). This is a bit more storage for this offset index, but for FDEs with lots of FREs this might be a worthwhile tradeoff.

Quoted 6 lines

Few points:
a) we can decide this "binary searchability" per-FDE, and for FDEs
with 1-2-3 FREs not bother, while those with more FREs would be searchable ones with index. So we can combine both fast lookups, natural alignment of on-disk format, and compactness. The presence of index is just another bit in FDE metadata.

I have been going back and forth on this one. So there seem to be the following options here:
#1. Make "binary searchability" a per-FDE decision.
#2. Make "binary searchability" a per-section decision (I expect
aarch64 to have very low number of FREs per FDE).
#3. Bake "binary searchability" into the SFrame FRE specification.
So its always ON for all FDEs. The advantage is that it makes stack tracers simpler to implement with less code.

I do think #2, #3 appear simpler in concept.

Quoted 8 lines

b) bitness of offset_idxs[] can be coupled with bitness of
start_addrs (for simplicity), or could be completely independent and identified by FDE's metadata (2 more bits to define this just like start_addr bitness is defined). Independent probably would be my preference, with linker (or whoever will be producing .sframe data) can pick the smallest bitness that is sufficient to represent everything.

ATM, GAS does apply special logic to decide the bitness of start_addrs per function, and ld just uses that info. Coupling the bitness of offset_idx with bitness of start_addrs will be easy (or _easier_ I think), but for now, I leave it as "should be doable" :)

Quoted 8 lines

Yes, it's a bit more complicated to draw and explain, but everything will be nicely aligned, extensible to 64 bits, and (optionally at least) binary searchable. Implementation-wise on the kernel side it shouldn't be significantly more involved. Maybe the compiler would need to be a bit smarter when producing FDE data, but it's no rocket science.

Thoughts?

Combining the requirements from your email and Josh's follow up:
- No unaligned accesses
- Sorted FREs

I would put compaction as a "good to have" requirement. It appears to me that any compaction will mean a sort of post-processing which will interfere with JIT usecase.

author maintainer
#67 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 10 lines · Indu Bhagat
Quoted 4 lines

Yeah, and it's actually bothering me quite a lot 🙂 I have a tentative proposal, maybe we can discuss this for SFrame v3? Let me briefly outline the idea.

I looked at the idea below. It could work wrt unaligned accesses.

Speaking of unaligned accesses, I will ask away: Is the reason to avoid unaligned accesses performance hit or are there other practical reasons to it ?

I think performance is the main concern, though there are still some CPU arches out there which don't support unaligned accesses.

Quoted 7 lines

Combining the requirements from your email and Josh's follow up:
- No unaligned accesses
- Sorted FREs

I would put compaction as a "good to have" requirement. It appears to me that any compaction will mean a sort of post-processing which will interfere with JIT usecase.

I think we should still consider the fast lookup table. We might want to prototype something just to see what the speedup looks like. Similar to compaction it could just be an optional feature implemented by the linker which JIT doesn't have to use.

Signature · 1 line
Josh
#68 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

In reply to Indu Bhagat
Quoted 45 lines · Indu Bhagat
Quoted 34 lines · Andrii Nakryiko
Quoted 34 lines
Quoted 34 lines
Quoted 23 lines
Quoted 23 lines
kernel/unwind/sframe.h new file +23 -0
+struct sframe_preamble {
+       u16     magic;
+       u8      version;
+       u8      flags;
+} __packed;
+
+struct sframe_header {
+       struct sframe_preamble preamble;
+       u8      abi_arch;
+       s8      cfa_fixed_fp_offset;
+       s8      cfa_fixed_ra_offset;
+       u8      auxhdr_len;
+       u32     num_fdes;
+       u32     num_fres;
+       u32     fre_len;
+       u32     fdes_off;
+       u32     fres_off;
+} __packed;
+
+struct sframe_fde {
+       s32     start_addr;
+       u32     func_size;
+       u32     fres_off;
Quoted 8 lines

kernel/unwind/sframe.h line 57

Quoted 5 lines
kernel/unwind/sframe.h new file +5 -0
+       u32     fres_num;
+       u8      info;
+       u8      rep_size;
+       u16 padding;
+} __packed;

I couldn't understand from SFrame itself, but why do sframe_header, sframe_preamble, and sframe_fde have to be marked __packed, if it's all naturally aligned (intentionally and by design)?..

Right, but the spec says they're all packed. Maybe the point is that some future sframe version is free to introduce unaligned fields.

Quoted 8 lines
Quoted 4 lines

SFrame specification aims to keep SFrame header and SFrame FDE members at aligned boundaries in future versions.

Only SFrame FRE related accesses may have unaligned accesses.

Yeah, and it's actually bothering me quite a lot 🙂 I have a tentative proposal, maybe we can discuss this for SFrame v3? Let me briefly outline the idea.

Quoted 5 lines

I looked at the idea below. It could work wrt unaligned accesses.

Speaking of unaligned accesses, I will ask away: Is the reason to avoid unaligned accesses performance hit or are there other practical reasons to it ?

Performance hit on architectures like x86-64 that do support unaligned, but it's actually a CPU error for some other architectures, so you'd need to code with that in mind, making local aligned copies, etc. In general, I'd say it's a bit of a red flag that a format that is meant to be memory-mapped (effectively) and used without pre-processing requires dealing with unaligned accesses. So if we can fix that, that would be a win.

Quoted 90 lines
Quoted 74 lines

So, currently in v2, FREs within FDEs use an array-of-structs layout. If we use preudo-C type definitions, it would be something like this for FDE + its FREs:

struct FDE_and_FREs {
     struct sframe_func_desc_entry fde_metadata;

     union FRE {
         struct FRE8 {
             u8 sfre_start_address;
             u8 sfre_info;
             u8|u16|u32 offsets[M];
         }
         struct FRE16 {
             u16 sfre_start_address;
             u16 sfre_info;
             u8|u16|u32 offsets[M];
         }
         struct FRE32 {
             u32 sfre_start_address;
             u32 sfre_info;
             u8|u16|u32 offsets[M];
         }
     } fres[N] __packed;
};

where all fres[i]s are one of those FRE8/FRE16/FRE32, so start addresses have the same size, but each FRE has potentially different offsets sizing, so there is no common alignment, and so everything has to be packed and unaligned.

But what if we take a struct-of-arrays approach and represent it more like:

struct FDE_and_FREs {
     struct sframe_func_desc_entry fde_metadata;
     u8|u16|u32 start_addrs[N]; /* can extend to u64 as well */
     u8 sfre_infos[N];
     u8 offsets8[M8];
     u16 offsets16[M16] __aligned(2);
     u32 offsets32[M32] __aligned(4);
     /* we can naturally extend to support also u64 offsets */
};

i.e., we split all FRE records into their three constituents: start addresses, info bytes, and then each FRE can fall into either 8-, 16-, or 32-bit offsets "bucket". We collect all the offsets, depending on their size, into these aligned offsets{8,16,32} arrays (with natural extension to 64 bits, if necessary), with at most wasting 1-3 bytes to ensure proper alignment everywhere.

Note, at this point we need to decide if we want to make FREs binary searchable or not.

If not, we don't really need anything extra. As we process each start_addrs[i] and sfre_infos[i] to find matching FRE, we keep track of how many 8-, 16-, and 32-bit offsets already processed FREs consumed, and when we find the right one, we know exactly the starting index within offset{8,16,32}. Done.

But if we were to make FREs binary searchable, we need to basically have an index of offset pointers to quickly find offsetsX[j] position corresponding to FRE #i. For that, we can have an extra array right next to start_addrs, "semantically parallel" to it:

u8|u16|u32 start_addrs[N];
u8|u16|u32 offset_idxs[N];

where start_addrs[i] corresponds to offset_idxs[i], and offset_idxs[i] points to the first offset corresponding to FRE #i in offsetX[] array (depending on FRE's "bitness"). This is a bit more storage for this offset index, but for FDEs with lots of FREs this might be a worthwhile tradeoff.

Quoted 6 lines

Few points:
a) we can decide this "binary searchability" per-FDE, and for FDEs
with 1-2-3 FREs not bother, while those with more FREs would be searchable ones with index. So we can combine both fast lookups, natural alignment of on-disk format, and compactness. The presence of index is just another bit in FDE metadata.

I have been going back and forth on this one. So there seem to be the following options here:
#1. Make "binary searchability" a per-FDE decision.
#2. Make "binary searchability" a per-section decision (I expect
aarch64 to have very low number of FREs per FDE).
#3. Bake "binary searchability" into the SFrame FRE specification.
So its always ON for all FDEs. The advantage is that it makes stack tracers simpler to implement with less code.

Quoted 1 line

I do think #2, #3 appear simpler in concept.

Whichever makes it easier across the entire stack (compiler, linker, kernel/unwinder). As long as binary searchability is possible, especially for FDEs with lots of FREs. Making it per-FDE just allows to pick most compact (but still with good performance) representation.

Quoted 10 lines
Quoted 9 lines

b) bitness of offset_idxs[] can be coupled with bitness of
start_addrs (for simplicity), or could be completely independent and identified by FDE's metadata (2 more bits to define this just like start_addr bitness is defined). Independent probably would be my preference, with linker (or whoever will be producing .sframe data) can pick the smallest bitness that is sufficient to represent everything.

Quoted 4 lines

ATM, GAS does apply special logic to decide the bitness of start_addrs per function, and ld just uses that info. Coupling the bitness of offset_idx with bitness of start_addrs will be easy (or _easier_ I think), but for now, I leave it as "should be doable" :)

Those offsets are relative to fde's start_addr, right? So, generally speaking, should be usually small? I my understanding is correct, then yeah, coupling is probably ok.

Quoted 10 lines
Quoted 9 lines

Yes, it's a bit more complicated to draw and explain, but everything will be nicely aligned, extensible to 64 bits, and (optionally at least) binary searchable. Implementation-wise on the kernel side it shouldn't be significantly more involved. Maybe the compiler would need to be a bit smarter when producing FDE data, but it's no rocket science.

Thoughts?

Quoted 8 lines

Combining the requirements from your email and Josh's follow up:
- No unaligned accesses
- Sorted FREs

I would put compaction as a "good to have" requirement. It appears to me that any compaction will mean a sort of post-processing which will interfere with JIT usecase.

sgtm

#69 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

In reply to Andrii Nakryiko
Quoted 26 lines · Andrii Nakryiko

So, currently in v2, FREs within FDEs use an array-of-structs layout. If we use preudo-C type definitions, it would be something like this for FDE + its FREs:

struct FDE_and_FREs {
     struct sframe_func_desc_entry fde_metadata;

     union FRE {
         struct FRE8 {
             u8 sfre_start_address;
             u8 sfre_info;
             u8|u16|u32 offsets[M];
         }
         struct FRE16 {
             u16 sfre_start_address;
             u16 sfre_info;
             u8|u16|u32 offsets[M];
         }
         struct FRE32 {
             u32 sfre_start_address;
             u32 sfre_info;
             u8|u16|u32 offsets[M];
         }
     } fres[N] __packed;
};
Quoted 4 lines

where all fres[i]s are one of those FRE8/FRE16/FRE32, so start addresses have the same size, but each FRE has potentially different offsets sizing, so there is no common alignment, and so everything has to be packed and unaligned.

Just for clarification of the SFrame V2 format, as there may be some misconception. Using pseudo-C type definition:

struct sframe_fre8 {
	u8 sfre_start_address;
	u8 sfre_info;
	s8|s16|s32 offsets[M];
};
struct sframe_fre16 {
	u16 sfre_start_address;
	u8 sfre_info;
	s8|s16|s32 offsets[M];
};
struct sframe_fre32 {
	u32 sfre_start_address;
	u8 sfre_info;
	s8|s16|s32 offsets[M];
};
struct sframe_section {
	/* Headers. */
	struct sframe_preamble preamble;
	struct sframe_header header;
	struct sframe_auxiliary_header auxhdr;

	/* FDEs. */
	struct sframe_fde fdes[N_FDE];

	/* FRE8s / FRE16s / FRE32s per FDE. */
	struct sframe_fre{8|16|32} fres_fde1[N_FRE_FDE1] __packed;
	...
	struct sframe_fre{8|16|32} fres_fdeN[N_FRE_FDEN] __packed;
};

Where:
- fdes[] can be binary searched: All fdes[i] are of equal size and sorted on start address.
- Each fdes[i] points at its fres_fdei[].
- fres_fdei[] cannot be binary searched: For each fdes[i] they are one of those FRE8/FRE16/FRE32, so start addresses have the same size, but each FRE has potentially different offsets sizing, so there is no common alignment, and so everything has to be packed and unaligned.

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
#70 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

Quoted 37 lines · Jens Remus
Quoted 26 lines · Andrii Nakryiko

So, currently in v2, FREs within FDEs use an array-of-structs layout. If we use preudo-C type definitions, it would be something like this for FDE + its FREs:

struct FDE_and_FREs {
     struct sframe_func_desc_entry fde_metadata;

     union FRE {
         struct FRE8 {
             u8 sfre_start_address;
             u8 sfre_info;
             u8|u16|u32 offsets[M];
         }
         struct FRE16 {
             u16 sfre_start_address;
             u16 sfre_info;
             u8|u16|u32 offsets[M];
         }
         struct FRE32 {
             u32 sfre_start_address;
             u32 sfre_info;
             u8|u16|u32 offsets[M];
         }
     } fres[N] __packed;
};
Quoted 4 lines

where all fres[i]s are one of those FRE8/FRE16/FRE32, so start addresses have the same size, but each FRE has potentially different offsets sizing, so there is no common alignment, and so everything has to be packed and unaligned.

Just for clarification of the SFrame V2 format, as there may be some misconception. Using pseudo-C type definition:

Quoted 16 lines
struct sframe_fre8 {
        u8 sfre_start_address;
        u8 sfre_info;
        s8|s16|s32 offsets[M];
};
struct sframe_fre16 {
        u16 sfre_start_address;
        u8 sfre_info;
        s8|s16|s32 offsets[M];
};
struct sframe_fre32 {
        u32 sfre_start_address;
        u8 sfre_info;
        s8|s16|s32 offsets[M];
};

yeah, sorry, copy/paste error for those sfre_info, it's always u8

Quoted 15 lines
struct sframe_section {
        /* Headers. */
        struct sframe_preamble preamble;
        struct sframe_header header;
        struct sframe_auxiliary_header auxhdr;

        /* FDEs. */
        struct sframe_fde fdes[N_FDE];

        /* FRE8s / FRE16s / FRE32s per FDE. */
        struct sframe_fre{8|16|32} fres_fde1[N_FRE_FDE1] __packed;
        ...
        struct sframe_fre{8|16|32} fres_fdeN[N_FRE_FDEN] __packed;
};
Quoted 10 lines

Where:
- fdes[] can be binary searched: All fdes[i] are of equal size and sorted on start address.
- Each fdes[i] points at its fres_fdei[].
- fres_fdei[] cannot be binary searched: For each fdes[i] they are one of those FRE8/FRE16/FRE32, so start addresses have the same size, but each FRE has potentially different offsets sizing, so there is no common alignment, and so everything has to be packed and unaligned.

yep

Quoted 13 lines

Regards,
Jens
--
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294 IBM Data Privacy Statement: https://www.ibm.com/privacy/

#71 lore raw

Re: [PATCH v4 17/39] unwind_user/sframe: Add support for reading .sframe headers

In reply to Andrii Nakryiko
Quoted 50 lines · Andrii Nakryiko
Quoted 20 lines · Josh Poimboeuf

In preparation for unwinding user space stacks with sframe, add basic sframe compile infrastructure and support for reading the .sframe section header.

sframe_add_section() reads the header and unconditionally returns an error, so it's not very useful yet. A subsequent patch will improve that.

---
  arch/Kconfig           |   3 +
  include/linux/sframe.h |  36 +++++++++++
  kernel/unwind/Makefile |   3 +-
  kernel/unwind/sframe.c | 136 +++++++++++++++++++++++++++++++++++++++++
  kernel/unwind/sframe.h |  71 +++++++++++++++++++++
  5 files changed, 248 insertions(+), 1 deletion(-)

create mode 100644 include/linux/sframe.h
create mode 100644 kernel/unwind/sframe.c
create mode 100644 kernel/unwind/sframe.h

[...]

include/linux/sframe.h line 31

Quoted 8 lines
include/linux/sframe.h new file +8 -0
+
+extern int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end,
+                             unsigned long text_start, unsigned long text_end);
+extern int sframe_remove_section(unsigned long sframe_addr);
+
+#else /* !CONFIG_HAVE_UNWIND_USER_SFRAME */
+
+static inline int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end, unsigned long text_start, unsigned long text_end) { return -ENOSYS; }

nit: very-very long, wrap it?

Quoted 5 lines
include/linux/sframe.h new file +5 -0
+static inline int sframe_remove_section(unsigned long sframe_addr) { return -ENOSYS; }
+
+#endif /* CONFIG_HAVE_UNWIND_USER_SFRAME */
+
+#endif /* _LINUX_SFRAME_H */

[...]

Quoted 10 lines
kernel/unwind/sframe.c new file +10 -0
+static int sframe_read_header(struct sframe_section *sec)
+{
+       unsigned long header_end, fdes_start, fdes_end, fres_start, fres_end;
+       struct sframe_header shdr;
+       unsigned int num_fdes;
+
+       if (copy_from_user(&shdr, (void __user *)sec->sframe_start, sizeof(shdr))) {
+               dbg("header usercopy failed\n");
+               return -EFAULT;
+       }
Quoted 8 lines

kernel/unwind/sframe.c line 40

Quoted 4 lines
kernel/unwind/sframe.c new file +4 -0
+
+       if (shdr.preamble.magic != SFRAME_MAGIC ||
+           shdr.preamble.version != SFRAME_VERSION_2 ||
+           !(shdr.preamble.flags & SFRAME_F_FDE_SORTED) ||

probably more a question to Indu, but why is this sorting not mandatory and part of SFrame "standard"? How realistically non-sorted FDEs would work in practice? Ain't nobody got time to sort them just to unwind the stack...

It is not worthwhile for the assembler (even wasteful as it adds to build time for nothing) to generate an .sframe section with FDEs in sorted order of start PC of function. This is because the final order is decided by the linker as it merges all input sections.

Thats one reason why it is already necessary that the specification allows SFRAME_F_FDE_SORTED not set in the section. I can also see how not making the sorting mandatory may also be necessary for JIT use-case..

FWIW, for non-JIT environments, non-sorted FDEs are not expected in linked binaries; such a thing does not seem to be useful in practice.

Hope that helps
Indu

author maintainer
#72 lore raw

PATCH v4 18/39 unwind_user/sframe: Store sframe section data in per-mm maple tree

In reply to Josh Poimboeuf

Associate an sframe section with its mm by adding it to a per-mm maple tree which is indexed by the corresponding text address range. A single sframe section can be associated with multiple text ranges.

---
 arch/x86/include/asm/mmu.h |  2 +-
 include/linux/mm_types.h   |  3 +++
 include/linux/sframe.h     | 13 +++++++++
 kernel/fork.c              | 10 +++++++
 kernel/unwind/sframe.c     | 55 +++++++++++++++++++++++++++++++++++---
 mm/init-mm.c               |  2 ++
 6 files changed, 81 insertions(+), 4 deletions(-)
arch/x86/include/asm/mmu.h +1 -1
index ce4677b8b735..12ea831978cc 100644
@@ -73,7 +73,7 @@ typedef struct {
 	.context = {							\
 		.ctx_id = 1,						\
 		.lock = __MUTEX_INITIALIZER(mm.context.lock),		\
-	}
+	},
 
 void leave_mm(void);
 #define leave_mm leave_mm
include/linux/mm_types.h +3 -0
index 332cee285662..aba0d67fda9a 100644
@@ -1064,6 +1064,9 @@ struct mm_struct {
 #endif
 		} lru_gen;
 #endif /* CONFIG_LRU_GEN_WALKS_MMU */
+#ifdef CONFIG_HAVE_UNWIND_USER_SFRAME
+		struct maple_tree sframe_mt;
+#endif
 	} __randomize_layout;
 
 	/*
include/linux/sframe.h +13 -0
index 3bfaf21869c2..ff4b9d1dbd00 100644
@@ -22,14 +22,27 @@ struct sframe_section {
 	signed char	fp_off;
 };
 
+#define INIT_MM_SFRAME .sframe_mt = MTREE_INIT(sframe_mt, 0),
+extern void sframe_free_mm(struct mm_struct *mm);
+
 extern int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end,
 			      unsigned long text_start, unsigned long text_end);
 extern int sframe_remove_section(unsigned long sframe_addr);
 
+static inline bool current_has_sframe(void)
+{
+	struct mm_struct *mm = current->mm;
+
+	return mm && !mtree_empty(&mm->sframe_mt);
+}
+
 #else /* !CONFIG_HAVE_UNWIND_USER_SFRAME */
 
+#define INIT_MM_SFRAME
+static inline void sframe_free_mm(struct mm_struct *mm) {}
 static inline int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end, unsigned long text_start, unsigned long text_end) { return -ENOSYS; }
 static inline int sframe_remove_section(unsigned long sframe_addr) { return -ENOSYS; }
+static inline bool current_has_sframe(void) { return false; }
 
 #endif /* CONFIG_HAVE_UNWIND_USER_SFRAME */
 
kernel/fork.c +10 -0
index 9b301180fd41..88753f8bbdd3 100644
@@ -105,6 +105,7 @@
 #include <uapi/linux/pidfd.h>
 #include <linux/pidfs.h>
 #include <linux/tick.h>
+#include <linux/sframe.h>
 
 #include <asm/pgalloc.h>
 #include <linux/uaccess.h>
@@ -925,6 +926,7 @@ void __mmdrop(struct mm_struct *mm)
 	mm_pasid_drop(mm);
 	mm_destroy_cid(mm);
 	percpu_counter_destroy_many(mm->rss_stat, NR_MM_COUNTERS);
+	sframe_free_mm(mm);
 
 	free_mm(mm);
 }
@@ -1252,6 +1254,13 @@ static void mm_init_uprobes_state(struct mm_struct *mm)
 #endif
 }
 
+static void mm_init_sframe(struct mm_struct *mm)
+{
+#ifdef CONFIG_HAVE_UNWIND_USER_SFRAME
+	mt_init(&mm->sframe_mt);
+#endif
+}
+
 static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p,
 	struct user_namespace *user_ns)
 {
@@ -1283,6 +1292,7 @@ static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p,
 	mm->pmd_huge_pte = NULL;
 #endif
 	mm_init_uprobes_state(mm);
+	mm_init_sframe(mm);
 	hugetlb_count_init(mm);
 
 	if (current->mm) {
kernel/unwind/sframe.c +52 -3

67 lines not on this page · raw message

mm/init-mm.c +2 -0
index 24c809379274..feb01fcd32f0 100644
@@ -11,6 +11,7 @@
 #include <linux/atomic.h>
 #include <linux/user_namespace.h>
 #include <linux/iommu.h>
+#include <linux/sframe.h>
 #include <asm/mmu.h>
 
 #ifndef INIT_MM_CONTEXT
@@ -45,6 +46,7 @@ struct mm_struct init_mm = {
 	.user_ns	= &init_user_ns,
 	.cpu_bitmap	= CPU_BITS_NONE,
 	INIT_MM_CONTEXT(init_mm)
+	INIT_MM_SFRAME
 };
 
 void setup_initial_init_mm(void *start_code, void *end_code,
Signature · 1 line
2.48.1
author maintainer
#73 lore raw

PATCH v4 19/39 unwind_user/sframe: Add support for reading .sframe contents

In reply to Josh Poimboeuf

In preparation for using sframe to unwind user space stacks, add an sframe_find() interface for finding the sframe information associated with a given text address.

For performance, use user_read_access_begin() and the corresponding unsafe_*() accessors. Note that use of pr_debug() in uaccess-enabled regions would break noinstr validation, so there aren't any debug messages yet. That will be added in a subsequent commit.

---
 include/linux/sframe.h       |   5 +
 kernel/unwind/sframe.c       | 295 ++++++++++++++++++++++++++++++++++-
 kernel/unwind/sframe_debug.h |  35 +++++
 3 files changed, 331 insertions(+), 4 deletions(-)
 create mode 100644 kernel/unwind/sframe_debug.h
include/linux/sframe.h +5 -0
index ff4b9d1dbd00..2e70085a1e89 100644
@@ -3,11 +3,14 @@
 #define _LINUX_SFRAME_H
 
 #include <linux/mm_types.h>
+#include <linux/srcu.h>
 #include <linux/unwind_user_types.h>
 
 #ifdef CONFIG_HAVE_UNWIND_USER_SFRAME
 
 struct sframe_section {
+	struct rcu_head	rcu;
+
 	unsigned long	sframe_start;
 	unsigned long	sframe_end;
 	unsigned long	text_start;
@@ -28,6 +31,7 @@ extern void sframe_free_mm(struct mm_struct *mm);
 extern int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end,
 			      unsigned long text_start, unsigned long text_end);
 extern int sframe_remove_section(unsigned long sframe_addr);
+extern int sframe_find(unsigned long ip, struct unwind_user_frame *frame);
 
 static inline bool current_has_sframe(void)
 {
@@ -42,6 +46,7 @@ static inline bool current_has_sframe(void)
 static inline void sframe_free_mm(struct mm_struct *mm) {}
 static inline int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end, unsigned long text_start, unsigned long text_end) { return -ENOSYS; }
 static inline int sframe_remove_section(unsigned long sframe_addr) { return -ENOSYS; }
+static inline int sframe_find(unsigned long ip, struct unwind_user_frame *frame) { return -ENOSYS; }
 static inline bool current_has_sframe(void) { return false; }
 
 #endif /* CONFIG_HAVE_UNWIND_USER_SFRAME */
kernel/unwind/sframe.c +291 -4

321 lines not on this page · raw message

kernel/unwind/sframe_debug.h new file +35 -0
new file mode 100644
index 000000000000..055c8c8fae24
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _SFRAME_DEBUG_H
+#define _SFRAME_DEBUG_H
+
+#include <linux/sframe.h>
+#include "sframe.h"
+
+#ifdef CONFIG_DYNAMIC_DEBUG
+
+#define dbg(fmt, ...)							\
+	pr_debug("%s (%d): " fmt, current->comm, current->pid, ##__VA_ARGS__)
+
+static __always_inline void dbg_print_header(struct sframe_section *sec)
+{
+	unsigned long fdes_end;
+
+	fdes_end = sec->fdes_start + (sec->num_fdes * sizeof(struct sframe_fde));
+
+	dbg("SEC: sframe:0x%lx-0x%lx text:0x%lx-0x%lx "
+	    "fdes:0x%lx-0x%lx fres:0x%lx-0x%lx "
+	    "ra_off:%d fp_off:%d\n",
+	    sec->sframe_start, sec->sframe_end, sec->text_start, sec->text_end,
+	    sec->fdes_start, fdes_end, sec->fres_start, sec->fres_end,
+	    sec->ra_off, sec->fp_off);
+}
+
+#else /* !CONFIG_DYNAMIC_DEBUG */
+
+#define dbg(args...)			no_printk(args)
+
+static inline void dbg_print_header(struct sframe_section *sec) {}
+
+#endif /* !CONFIG_DYNAMIC_DEBUG */
+
+#endif /* _SFRAME_DEBUG_H */
Signature · 1 line
2.48.1
#74 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 4 lines · Josh Poimboeuf
include/linux/sframe.h +0 -0
@@ -3,11 +3,14 @@
  #define _LINUX_SFRAME_H

include/linux/sframe.h line 13

Quoted 10 lines
include/linux/sframe.h +3 -0
 
  #include <linux/mm_types.h>
+#include <linux/srcu.h>
  #include <linux/unwind_user_types.h>
 
  #ifdef CONFIG_HAVE_UNWIND_USER_SFRAME
 
  struct sframe_section {
+	struct rcu_head	rcu;
+

Nit: You are adding a blank line, that you later remove with "[PATCH v4 25/39] unwind_user/sframe: Show file name in debug output".

include/linux/sframe.h line 16

Quoted 3 lines
include/linux/sframe.h +0 -0
  	unsigned long	sframe_start;
  	unsigned long	sframe_end;
  	unsigned long	text_start;

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#75 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 4 lines · Jens Remus
Quoted 1 line · Josh Poimboeuf
include/linux/sframe.h +0 -0
Quoted 12 lines

include/linux/sframe.h line 10

Quoted 9 lines
include/linux/sframe.h +3 -0
@@ -3,11 +3,14 @@
  #define _LINUX_SFRAME_H
  #include <linux/mm_types.h>
+#include <linux/srcu.h>
  #include <linux/unwind_user_types.h>
  #ifdef CONFIG_HAVE_UNWIND_USER_SFRAME
  struct sframe_section {
+	struct rcu_head	rcu;
+

Nit: You are adding a blank line, that you later remove with "[PATCH v4 25/39] unwind_user/sframe: Show file name in debug output".

I suppose that was intentional. The original blank line created visual separation between the rcu head and the sframe values. The later patch instead sort of uses the ifdef to keep some separation? But yeah, I'll keep the blank lines for consistency. <shrug>

struct sframe_section {
	struct rcu_head	rcu;
#ifdef CONFIG_DYNAMIC_DEBUG
	const char	*filename;

#endif

	unsigned long	sframe_start;
	unsigned long	sframe_start;
	unsigned long	sframe_end;
	unsigned long	text_start;
	unsigned long	text_end;

	unsigned long	fdes_start;
	unsigned long	fres_start;
	unsigned long	fres_end;
	unsigned int	num_fdes;

	signed char	ra_off;
	signed char	fp_off;
};
Signature · 1 line
Josh
#76 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

In reply to Josh Poimboeuf
Quoted 18 lines · Josh Poimboeuf

In preparation for using sframe to unwind user space stacks, add an sframe_find() interface for finding the sframe information associated with a given text address.

For performance, use user_read_access_begin() and the corresponding unsafe_*() accessors. Note that use of pr_debug() in uaccess-enabled regions would break noinstr validation, so there aren't any debug messages yet. That will be added in a subsequent commit.

---
 include/linux/sframe.h       |   5 +
 kernel/unwind/sframe.c       | 295 ++++++++++++++++++++++++++++++++++-
 kernel/unwind/sframe_debug.h |  35 +++++
 3 files changed, 331 insertions(+), 4 deletions(-)
 create mode 100644 kernel/unwind/sframe_debug.h

[...]

kernel/unwind/sframe.c line 56

Quoted 13 lines
kernel/unwind/sframe.c +13 -0
+
+static __always_inline int __read_fde(struct sframe_section *sec,
+                                     unsigned int fde_num,
+                                     struct sframe_fde *fde)
+{
+       unsigned long fde_addr, ip;
+
+       fde_addr = sec->fdes_start + (fde_num * sizeof(struct sframe_fde));
+       unsafe_copy_from_user(fde, (void __user *)fde_addr,
+                             sizeof(struct sframe_fde), Efault);
+
+       ip = sec->sframe_start + fde->start_addr;
+       if (ip < sec->text_start || ip > sec->text_end)

ip >= sec->test_end ? ip == sec->test_end doesn't make sense, no?

Quoted 8 lines
kernel/unwind/sframe.c +8 -0
+               return -EINVAL;
+
+       return 0;
+
+Efault:
+       return -EFAULT;
+}
+

[...]

Quoted 8 lines
kernel/unwind/sframe.c +8 -0
+static __always_inline int __read_fre(struct sframe_section *sec,
+                                     struct sframe_fde *fde,
+                                     unsigned long fre_addr,
+                                     struct sframe_fre *fre)
+{
+       unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+       unsigned char fre_type = SFRAME_FUNC_FRE_TYPE(fde->info);
+       unsigned char offset_count, offset_size;

kernel/unwind/sframe.c line 164

Quoted 10 lines
kernel/unwind/sframe.c +10 -0
+       s32 ip_off, cfa_off, ra_off, fp_off;
+       unsigned long cur = fre_addr;
+       unsigned char addr_size;
+       u8 info;
+
+       addr_size = fre_type_to_size(fre_type);
+       if (!addr_size)
+               return -EFAULT;
+
+       if (fre_addr + addr_size + 1 > sec->fres_end)

nit: isn't this the same as fre_addr + addr_size >= sec->fres_end ?

kernel/unwind/sframe.c line 168

Quoted 4 lines
kernel/unwind/sframe.c +4 -0
+               return -EFAULT;
+
+       UNSAFE_GET_USER_INC(ip_off, cur, addr_size, Efault);
+       if (fde_type == SFRAME_FDE_TYPE_PCINC && ip_off > fde->func_size)

is ip_off == fde->func_size allowable?

kernel/unwind/sframe.c line 177

Quoted 9 lines
kernel/unwind/sframe.c +9 -0
+               return -EFAULT;
+
+       UNSAFE_GET_USER_INC(info, cur, 1, Efault);
+       offset_count = SFRAME_FRE_OFFSET_COUNT(info);
+       offset_size  = offset_size_enum_to_size(SFRAME_FRE_OFFSET_SIZE(info));
+       if (!offset_count || !offset_size)
+               return -EFAULT;
+
+       if (cur + (offset_count * offset_size) > sec->fres_end)

offset_count * offset_size done in u8 can overflow, no? maybe upcast to unsigned long or use check_add_overflow?

Quoted 33 lines
kernel/unwind/sframe.c +33 -0
+               return -EFAULT;
+
+       fre->size = addr_size + 1 + (offset_count * offset_size);
+
+       UNSAFE_GET_USER_INC(cfa_off, cur, offset_size, Efault);
+       offset_count--;
+
+       ra_off = sec->ra_off;
+       if (!ra_off) {
+               if (!offset_count--)
+                       return -EFAULT;
+
+               UNSAFE_GET_USER_INC(ra_off, cur, offset_size, Efault);
+       }
+
+       fp_off = sec->fp_off;
+       if (!fp_off && offset_count) {
+               offset_count--;
+               UNSAFE_GET_USER_INC(fp_off, cur, offset_size, Efault);
+       }
+
+       if (offset_count)
+               return -EFAULT;
+
+       fre->ip_off             = ip_off;
+       fre->cfa_off            = cfa_off;
+       fre->ra_off             = ra_off;
+       fre->fp_off             = fp_off;
+       fre->info               = info;
+
+       return 0;
+
+Efault:

kernel/unwind/sframe.c line 220

Quoted 10 lines
kernel/unwind/sframe.c +10 -0
+       return -EFAULT;
+}
+
+static __always_inline int __find_fre(struct sframe_section *sec,
+                                     struct sframe_fde *fde, unsigned long ip,
+                                     struct unwind_user_frame *frame)
+{
+       unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+       struct sframe_fre *fre, *prev_fre = NULL;
+       struct sframe_fre fres[2];

you only need prev_fre->ip_off, so why all this which and fres[2] business if all you need is prev_fre_off and a bool whether you have prev_fre at all?

kernel/unwind/sframe.c line 229

Quoted 9 lines
kernel/unwind/sframe.c +9 -0
+       unsigned long fre_addr;
+       bool which = false;
+       unsigned int i;
+       s32 ip_off;
+
+       ip_off = (s32)(ip - sec->sframe_start) - fde->start_addr;
+
+       if (fde_type == SFRAME_FDE_TYPE_PCMASK)
+               ip_off %= fde->rep_size;

did you check that fde->rep_size is not zero?

kernel/unwind/sframe.c line 233

Quoted 4 lines
kernel/unwind/sframe.c +4 -0
+
+       fre_addr = sec->fres_start + fde->fres_off;
+
+       for (i = 0; i < fde->fres_num; i++) {

why not binary search? seem more logical to guard against cases with lots of FREs and be pretty fast in common case anyways.

Quoted 36 lines
kernel/unwind/sframe.c +36 -0
+               int ret;
+
+               /*
+                * Alternate between the two fre_addr[] entries for 'fre' and
+                * 'prev_fre'.
+                */
+               fre = which ? fres : fres + 1;
+               which = !which;
+
+               ret = __read_fre(sec, fde, fre_addr, fre);
+               if (ret)
+                       return ret;
+
+               fre_addr += fre->size;
+
+               if (prev_fre && fre->ip_off <= prev_fre->ip_off)
+                       return -EFAULT;
+
+               if (fre->ip_off > ip_off)
+                       break;
+
+               prev_fre = fre;
+       }
+
+       if (!prev_fre)
+               return -EINVAL;
+       fre = prev_fre;
+
+       frame->cfa_off = fre->cfa_off;
+       frame->ra_off  = fre->ra_off;
+       frame->fp_off  = fre->fp_off;
+       frame->use_fp  = SFRAME_FRE_CFA_BASE_REG_ID(fre->info) == SFRAME_BASE_REG_FP;
+
+       return 0;
+}
+

[...]

author maintainer
#77 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 15 lines · Andrii Nakryiko

kernel/unwind/sframe.c line 56

Quoted 12 lines · Josh Poimboeuf
kernel/unwind/sframe.c +12 -0
+static __always_inline int __read_fde(struct sframe_section *sec,
+                                     unsigned int fde_num,
+                                     struct sframe_fde *fde)
+{
+       unsigned long fde_addr, ip;
+
+       fde_addr = sec->fdes_start + (fde_num * sizeof(struct sframe_fde));
+       unsafe_copy_from_user(fde, (void __user *)fde_addr,
+                             sizeof(struct sframe_fde), Efault);
+
+       ip = sec->sframe_start + fde->start_addr;
+       if (ip < sec->text_start || ip > sec->text_end)

ip >= sec->test_end ? ip == sec->test_end doesn't make sense, no?

Believe it or not, this may have been intentional: 'text_end' can actually be a valid stack return address if the last instruction of the section is a call to a noreturn function. The unwinder still needs to know the state of the stack at that point.

That said, there would be no reason to put an FDE at the end. So yeah, it should be '>='.

Quoted 12 lines
Quoted 12 lines
kernel/unwind/sframe.c +12 -0
+static __always_inline int __read_fre(struct sframe_section *sec,
+                                     struct sframe_fde *fde,
+                                     unsigned long fre_addr,
+                                     struct sframe_fre *fre)
+{
+       unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+       unsigned char fre_type = SFRAME_FUNC_FRE_TYPE(fde->info);
+       unsigned char offset_count, offset_size;
+       s32 ip_off, cfa_off, ra_off, fp_off;
+       unsigned long cur = fre_addr;
+       unsigned char addr_size;
+       u8 info;
Quoted 8 lines

kernel/unwind/sframe.c line 164

Quoted 6 lines
kernel/unwind/sframe.c +6 -0
+
+       addr_size = fre_type_to_size(fre_type);
+       if (!addr_size)
+               return -EFAULT;
+
+       if (fre_addr + addr_size + 1 > sec->fres_end)

nit: isn't this the same as fre_addr + addr_size >= sec->fres_end ?

Yes, but that would further obfuscate the fact that this is validating that the next two reads don't go past sec->fres_end:

	UNSAFE_GET_USER_INC(ip_off, cur, addr_size, Efault);
	UNSAFE_GET_USER_INC(info, cur, 1, Efault);

The explicit "1" is for that second read.

I'll do something like SFRAME_FRE_INFO_SIZE instead of 1 to make that clearer.

Quoted 6 lines

kernel/unwind/sframe.c line 168

Quoted 4 lines
kernel/unwind/sframe.c +4 -0
+               return -EFAULT;
+
+       UNSAFE_GET_USER_INC(ip_off, cur, addr_size, Efault);
+       if (fde_type == SFRAME_FDE_TYPE_PCINC && ip_off > fde->func_size)

is ip_off == fde->func_size allowable?

This was another one where I was probably thinking about the whole "last insn could be a call to noreturn" scenario.

But even in that case, the frame would be unchanged after the call, so there shouldn't need to be an FRE there.

In fact, for the unwinder to deal with that scenario, for a call frame (as opposed to an interrupted one), it should always subtract one from the return address before calling into sframe so that it points to the calling instruction. </me makes note to go do that>

So yeah, this looks like another off-by-one, caused by overthinking yet not thinking enough.

Quoted 4 lines
Quoted 4 lines
kernel/unwind/sframe.c +4 -0
+               return -EFAULT;
+
+       UNSAFE_GET_USER_INC(info, cur, 1, Efault);
+       offset_count = SFRAME_FRE_OFFSET_COUNT(info);
Quoted 8 lines

kernel/unwind/sframe.c line 177

Quoted 5 lines
kernel/unwind/sframe.c +5 -0
+       offset_size  = offset_size_enum_to_size(SFRAME_FRE_OFFSET_SIZE(info));
+       if (!offset_count || !offset_size)
+               return -EFAULT;
+
+       if (cur + (offset_count * offset_size) > sec->fres_end)

offset_count * offset_size done in u8 can overflow, no? maybe upcast to unsigned long or use check_add_overflow?

offset_size is <= 2 as returned by offset_size_enum_to_size().

offset_count is expected to be <= 3, enforced by the !offset_count check at the bottom.

An overflow here would be harmless as it would be caught by the !offset_count anyway. Though I also notice offset_count isn't big enough to hold the 2-byte SFRAME_FRE_OFFSET_COUNT() value. Which is harmless for the same reason, but yeah I'll make offset_count an unsigned int.

Quoted 11 lines

kernel/unwind/sframe.c line 220

Quoted 7 lines
kernel/unwind/sframe.c +7 -0
+static __always_inline int __find_fre(struct sframe_section *sec,
+                                     struct sframe_fde *fde, unsigned long ip,
+                                     struct unwind_user_frame *frame)
+{
+       unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+       struct sframe_fre *fre, *prev_fre = NULL;
+       struct sframe_fre fres[2];

you only need prev_fre->ip_off, so why all this which and fres[2] business if all you need is prev_fre_off and a bool whether you have prev_fre at all?

So this cleverness probably needs a comment. prev_fre is actually needed after the loop:

Quoted 3 lines

kernel/unwind/sframe.c line 260

Quoted 3 lines
kernel/unwind/sframe.c +3 -0
+       if (!prev_fre)
+               return -EINVAL;
+       fre = prev_fre;

In the body of the loop, prev_fre is a tentative match, unless the next fre also matches, in which case that one becomes the new tentative match.

I'll add a comment. Also it'll probably be less confusing if I rename "prev_fre" to "fre", and "fre" to "next_fre".

Quoted 11 lines

kernel/unwind/sframe.c line 229

Quoted 9 lines
kernel/unwind/sframe.c +9 -0
+       unsigned long fre_addr;
+       bool which = false;
+       unsigned int i;
+       s32 ip_off;
+
+       ip_off = (s32)(ip - sec->sframe_start) - fde->start_addr;
+
+       if (fde_type == SFRAME_FDE_TYPE_PCMASK)
+               ip_off %= fde->rep_size;

did you check that fde->rep_size is not zero?

I did not, good catch!

Quoted 6 lines

kernel/unwind/sframe.c line 233

Quoted 3 lines
kernel/unwind/sframe.c +3 -0
+       fre_addr = sec->fres_start + fde->fres_off;
+
+       for (i = 0; i < fde->fres_num; i++) {

why not binary search? seem more logical to guard against cases with lots of FREs and be pretty fast in common case anyways.

That would be nice, but the FREs are variable-sized and you don't know how big one is until you start reading it.

Signature · 1 line
Josh
#78 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 18 lines · Josh Poimboeuf
Quoted 15 lines · Andrii Nakryiko

kernel/unwind/sframe.c line 56

Quoted 12 lines · Josh Poimboeuf
kernel/unwind/sframe.c +12 -0
+static __always_inline int __read_fde(struct sframe_section *sec,
+                                     unsigned int fde_num,
+                                     struct sframe_fde *fde)
+{
+       unsigned long fde_addr, ip;
+
+       fde_addr = sec->fdes_start + (fde_num * sizeof(struct sframe_fde));
+       unsafe_copy_from_user(fde, (void __user *)fde_addr,
+                             sizeof(struct sframe_fde), Efault);
+
+       ip = sec->sframe_start + fde->start_addr;
+       if (ip < sec->text_start || ip > sec->text_end)

ip >= sec->test_end ? ip == sec->test_end doesn't make sense, no?

Quoted 5 lines

Believe it or not, this may have been intentional: 'text_end' can actually be a valid stack return address if the last instruction of the section is a call to a noreturn function. The unwinder still needs to know the state of the stack at that point.

TIL...

Quoted 27 lines

That said, there would be no reason to put an FDE at the end. So yeah, it should be '>='.

Quoted 12 lines
Quoted 12 lines
kernel/unwind/sframe.c +12 -0
+static __always_inline int __read_fre(struct sframe_section *sec,
+                                     struct sframe_fde *fde,
+                                     unsigned long fre_addr,
+                                     struct sframe_fre *fre)
+{
+       unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+       unsigned char fre_type = SFRAME_FUNC_FRE_TYPE(fde->info);
+       unsigned char offset_count, offset_size;
+       s32 ip_off, cfa_off, ra_off, fp_off;
+       unsigned long cur = fre_addr;
+       unsigned char addr_size;
+       u8 info;
Quoted 8 lines

kernel/unwind/sframe.c line 164

Quoted 6 lines
kernel/unwind/sframe.c +6 -0
+
+       addr_size = fre_type_to_size(fre_type);
+       if (!addr_size)
+               return -EFAULT;
+
+       if (fre_addr + addr_size + 1 > sec->fres_end)

nit: isn't this the same as fre_addr + addr_size >= sec->fres_end ?

Yes, but that would further obfuscate the fact that this is validating that the next two reads don't go past sec->fres_end:

Quoted 7 lines
        UNSAFE_GET_USER_INC(ip_off, cur, addr_size, Efault);
        UNSAFE_GET_USER_INC(info, cur, 1, Efault);

The explicit "1" is for that second read.

I'll do something like SFRAME_FRE_INFO_SIZE instead of 1 to make that clearer.

yeah, I definitely didn't connect 1 with the size of fre info byte, named #define makes sense, thanks!

Quoted 37 lines
Quoted 7 lines

kernel/unwind/sframe.c line 168

Quoted 5 lines
kernel/unwind/sframe.c +4 -0
+               return -EFAULT;
+
+       UNSAFE_GET_USER_INC(ip_off, cur, addr_size, Efault);
+       if (fde_type == SFRAME_FDE_TYPE_PCINC && ip_off > fde->func_size)

is ip_off == fde->func_size allowable?

This was another one where I was probably thinking about the whole "last insn could be a call to noreturn" scenario.

But even in that case, the frame would be unchanged after the call, so there shouldn't need to be an FRE there.

In fact, for the unwinder to deal with that scenario, for a call frame (as opposed to an interrupted one), it should always subtract one from the return address before calling into sframe so that it points to the calling instruction. </me makes note to go do that>

So yeah, this looks like another off-by-one, caused by overthinking yet not thinking enough.

Quoted 4 lines
Quoted 4 lines
kernel/unwind/sframe.c +4 -0
+               return -EFAULT;
+
+       UNSAFE_GET_USER_INC(info, cur, 1, Efault);
+       offset_count = SFRAME_FRE_OFFSET_COUNT(info);
Quoted 8 lines

kernel/unwind/sframe.c line 177

Quoted 5 lines
kernel/unwind/sframe.c +5 -0
+       offset_size  = offset_size_enum_to_size(SFRAME_FRE_OFFSET_SIZE(info));
+       if (!offset_count || !offset_size)
+               return -EFAULT;
+
+       if (cur + (offset_count * offset_size) > sec->fres_end)

offset_count * offset_size done in u8 can overflow, no? maybe upcast to unsigned long or use check_add_overflow?

offset_size is <= 2 as returned by offset_size_enum_to_size().

Quoted 8 lines

offset_count is expected to be <= 3, enforced by the !offset_count check at the bottom.

An overflow here would be harmless as it would be caught by the !offset_count anyway. Though I also notice offset_count isn't big enough to hold the 2-byte SFRAME_FRE_OFFSET_COUNT() value. Which is harmless for the same reason, but yeah I'll make offset_count an unsigned int.

yeah, the less whoever is reading has to worry about overflows, the better, thanks!

Quoted 20 lines
Quoted 12 lines

kernel/unwind/sframe.c line 220

Quoted 8 lines
kernel/unwind/sframe.c +7 -0
+static __always_inline int __find_fre(struct sframe_section *sec,
+                                     struct sframe_fde *fde, unsigned long ip,
+                                     struct unwind_user_frame *frame)
+{
+       unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+       struct sframe_fre *fre, *prev_fre = NULL;
+       struct sframe_fre fres[2];

you only need prev_fre->ip_off, so why all this which and fres[2] business if all you need is prev_fre_off and a bool whether you have prev_fre at all?

So this cleverness probably needs a comment. prev_fre is actually needed after the loop:

Quoted 3 lines

kernel/unwind/sframe.c line 260

Quoted 3 lines
kernel/unwind/sframe.c +3 -0
+       if (!prev_fre)
+               return -EINVAL;
+       fre = prev_fre;
Quoted 6 lines

In the body of the loop, prev_fre is a tentative match, unless the next fre also matches, in which case that one becomes the new tentative match.

I'll add a comment. Also it'll probably be less confusing if I rename "prev_fre" to "fre", and "fre" to "next_fre".

ah, yeah, I missed that, you are right

Quoted 19 lines
Quoted 12 lines

kernel/unwind/sframe.c line 229

Quoted 10 lines
kernel/unwind/sframe.c +9 -0
+       unsigned long fre_addr;
+       bool which = false;
+       unsigned int i;
+       s32 ip_off;
+
+       ip_off = (s32)(ip - sec->sframe_start) - fde->start_addr;
+
+       if (fde_type == SFRAME_FDE_TYPE_PCMASK)
+               ip_off %= fde->rep_size;

did you check that fde->rep_size is not zero?

I did not, good catch!

Quoted 4 lines
Quoted 3 lines
kernel/unwind/sframe.c +3 -0
+       fre_addr = sec->fres_start + fde->fres_off;
+
+       for (i = 0; i < fde->fres_num; i++) {
Quoted 5 lines
Quoted 2 lines

why not binary search? seem more logical to guard against cases with lots of FREs and be pretty fast in common case anyways.

That would be nice, but the FREs are variable-sized and you don't know how big one is until you start reading it.

ah, another non-obvious thing, yeah... do you think it's worth fixing this and making FREs binary searchable in v3?

Indu, do you have some stats on distribution of FRE count per FDE in practice?

Tbh, FRE format is bothering me quite a lot... but let's discuss that in another thread with you and Indu

Quoted 3 lines

--
Josh

#79 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 12 lines · Andrii Nakryiko
Quoted 11 lines · Josh Poimboeuf
Quoted 10 lines · Andrii Nakryiko
Quoted 7 lines · Josh Poimboeuf
kernel/unwind/sframe.c +7 -0
+       UNSAFE_GET_USER_INC(info, cur, 1, Efault);
+       offset_count = SFRAME_FRE_OFFSET_COUNT(info);
+       offset_size  = offset_size_enum_to_size(SFRAME_FRE_OFFSET_SIZE(info));
+       if (!offset_count || !offset_size)
+               return -EFAULT;
+
+       if (cur + (offset_count * offset_size) > sec->fres_end)
Quoted 2 lines
Quoted 2 lines
Quoted 2 lines

offset_count * offset_size done in u8 can overflow, no? maybe upcast to unsigned long or use check_add_overflow?

The maximum offset_count * offset_size is 15 * 4 = 60 if I am not wrong:

Quoted 1 line
Quoted 1 line

offset_size is <= 2 as returned by offset_size_enum_to_size().

SFrame V2 FRE offset sizes are either 1, 2, or 4 bytes. This is also reflected in offset_size_enum_to_size().

Quoted 2 lines
Quoted 2 lines

offset_count is expected to be <= 3, enforced by the !offset_count check at the bottom.

SFrame V2 FRE offset count is 4 bits unsigned, so 0 <= offset_count <= 15.

Quoted 5 lines
Quoted 5 lines

An overflow here would be harmless as it would be caught by the !offset_count anyway. Though I also notice offset_count isn't big enough to hold the 2-byte SFRAME_FRE_OFFSET_COUNT() value. Which is harmless for the same reason, but yeah I'll make offset_count an unsigned int.

As mentioned above the FRE offset count is 4 bits, not 2 bytes. This is also reflected in SFRAME_FRE_OFFSET_COUNT().

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#80 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 27 lines · Jens Remus
Quoted 12 lines · Andrii Nakryiko
Quoted 11 lines · Josh Poimboeuf
Quoted 10 lines · Andrii Nakryiko
Quoted 7 lines · Josh Poimboeuf
kernel/unwind/sframe.c +7 -0
+       UNSAFE_GET_USER_INC(info, cur, 1, Efault);
+       offset_count = SFRAME_FRE_OFFSET_COUNT(info);
+       offset_size  = offset_size_enum_to_size(SFRAME_FRE_OFFSET_SIZE(info));
+       if (!offset_count || !offset_size)
+               return -EFAULT;
+
+       if (cur + (offset_count * offset_size) > sec->fres_end)
Quoted 2 lines
Quoted 2 lines
Quoted 2 lines

offset_count * offset_size done in u8 can overflow, no? maybe upcast to unsigned long or use check_add_overflow?

The maximum offset_count * offset_size is 15 * 4 = 60 if I am not wrong:

Quoted 1 line
Quoted 1 line

offset_size is <= 2 as returned by offset_size_enum_to_size().

SFrame V2 FRE offset sizes are either 1, 2, or 4 bytes. This is also reflected in offset_size_enum_to_size().

Quoted 2 lines
Quoted 2 lines

offset_count is expected to be <= 3, enforced by the !offset_count check at the bottom.

SFrame V2 FRE offset count is 4 bits unsigned, so 0 <= offset_count <= 15.

Quoted 8 lines
Quoted 5 lines
Quoted 5 lines

An overflow here would be harmless as it would be caught by the !offset_count anyway. Though I also notice offset_count isn't big enough to hold the 2-byte SFRAME_FRE_OFFSET_COUNT() value. Which is harmless for the same reason, but yeah I'll make offset_count an unsigned int.

As mentioned above the FRE offset count is 4 bits, not 2 bytes. This is also reflected in SFRAME_FRE_OFFSET_COUNT().

You are right on both counts, not sure what I was smoking that day.

Signature · 1 line
Josh
#81 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

In reply to Andrii Nakryiko
Quoted 14 lines · Andrii Nakryiko
Quoted 7 lines · Josh Poimboeuf
Quoted 6 lines · Andrii Nakryiko
Quoted 3 lines · Josh Poimboeuf
kernel/unwind/sframe.c +3 -0
+       fre_addr = sec->fres_start + fde->fres_off;
+
+       for (i = 0; i < fde->fres_num; i++) {
Quoted 5 lines
Quoted 2 lines

why not binary search? seem more logical to guard against cases with lots of FREs and be pretty fast in common case anyways.

That would be nice, but the FREs are variable-sized and you don't know how big one is until you start reading it.

Quoted 7 lines

ah, another non-obvious thing, yeah... do you think it's worth fixing this and making FREs binary searchable in v3?

Indu, do you have some stats on distribution of FRE count per FDE in practice?

Tbh, FRE format is bothering me quite a lot... but let's discuss that in another thread with you and Indu

I would be interested to be part of that discussion. I could share some preliminary stats from my s390 work.

Thanks and regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
#82 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

In reply to Josh Poimboeuf
Quoted 21 lines · Josh Poimboeuf
Quoted 12 lines · Andrii Nakryiko

kernel/unwind/sframe.c line 220

Quoted 7 lines · Josh Poimboeuf
kernel/unwind/sframe.c +7 -0
+static __always_inline int __find_fre(struct sframe_section *sec,
+                                     struct sframe_fde *fde, unsigned long ip,
+                                     struct unwind_user_frame *frame)
+{
+       unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+       struct sframe_fre *fre, *prev_fre = NULL;
+       struct sframe_fre fres[2];

you only need prev_fre->ip_off, so why all this which and fres[2] business if all you need is prev_fre_off and a bool whether you have prev_fre at all?

So this cleverness probably needs a comment. prev_fre is actually needed after the loop:

Quoted 3 lines

kernel/unwind/sframe.c line 260

Quoted 3 lines
kernel/unwind/sframe.c +3 -0
+       if (!prev_fre)
+               return -EINVAL;
+       fre = prev_fre;
Quoted 7 lines

In the body of the loop, prev_fre is a tentative match, unless the next fre also matches, in which case that one becomes the new tentative match.

I'll add a comment. Also it'll probably be less confusing if I rename "prev_fre" to "fre", and "fre" to "next_fre".

Nit: swap() might be a simplify way to alternate pointers between two fre_addr[] entries.

For example,

static __always_inline int __find_fre(struct sframe_section *sec,
struct sframe_fde *fde, unsigned long ip, struct unwind_user_frame *frame)

{
	/* intialize fres[] with invalid value */
	struct sframe_fre fres[2] = {0};
	struct sframe_fre *fre = &fres[1], *prev_fre = fres;

	for (i = 0; i < fde->fres_num; i++) {
		swap(fre, next_fre);
		ret = __read_fre(sec, fde, fre_addr, fre);

...

		if (fre->ip_off > ip_off)
			break;
	}

	if (fre->size == 0)
		return -EINVAL;

...

}

author maintainer
#83 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 8 lines · Weinan Liu

Nit: swap() might be a simplify way to alternate pointers between two fre_addr[] entries.

For example,

static __always_inline int __find_fre(struct sframe_section *sec,
struct sframe_fde *fde, unsigned long ip, struct unwind_user_frame *frame)

Quoted 8 lines
{
	/* intialize fres[] with invalid value */
	struct sframe_fre fres[2] = {0};
	struct sframe_fre *fre = &fres[1], *prev_fre = fres;

	for (i = 0; i < fde->fres_num; i++) {
		swap(fre, next_fre);
		ret = __read_fre(sec, fde, fre_addr, fre);

Problem is, if it breaks out early here on the first iteration:

Quoted 6 lines
		if (fre->ip_off > ip_off)
			break;
	}

	if (fre->size == 0)
		return -EINVAL;

Then fre isn't valid even though it has a nonzero size.

Signature · 1 line
Josh
#84 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

In reply to Josh Poimboeuf
Quoted 281 lines · Josh Poimboeuf

In preparation for using sframe to unwind user space stacks, add an sframe_find() interface for finding the sframe information associated with a given text address.

For performance, use user_read_access_begin() and the corresponding unsafe_*() accessors. Note that use of pr_debug() in uaccess-enabled regions would break noinstr validation, so there aren't any debug messages yet. That will be added in a subsequent commit.

---
  include/linux/sframe.h       |   5 +
  kernel/unwind/sframe.c       | 295 ++++++++++++++++++++++++++++++++++-
  kernel/unwind/sframe_debug.h |  35 +++++
  3 files changed, 331 insertions(+), 4 deletions(-)

create mode 100644 kernel/unwind/sframe_debug.h

include/linux/sframe.h +5 -0
index ff4b9d1dbd00..2e70085a1e89 100644
@@ -3,11 +3,14 @@
  #define _LINUX_SFRAME_H
 
  #include <linux/mm_types.h>
+#include <linux/srcu.h>
  #include <linux/unwind_user_types.h>
 
  #ifdef CONFIG_HAVE_UNWIND_USER_SFRAME
 
  struct sframe_section {
+	struct rcu_head	rcu;
+
  	unsigned long	sframe_start;
  	unsigned long	sframe_end;
  	unsigned long	text_start;
@@ -28,6 +31,7 @@ extern void sframe_free_mm(struct mm_struct *mm);
  extern int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end,
  			      unsigned long text_start, unsigned long text_end);
  extern int sframe_remove_section(unsigned long sframe_addr);
+extern int sframe_find(unsigned long ip, struct unwind_user_frame *frame);
 
  static inline bool current_has_sframe(void)
  {
@@ -42,6 +46,7 @@ static inline bool current_has_sframe(void)
  static inline void sframe_free_mm(struct mm_struct *mm) {}
  static inline int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end, unsigned long text_start, unsigned long text_end) { return -ENOSYS; }
  static inline int sframe_remove_section(unsigned long sframe_addr) { return -ENOSYS; }
+static inline int sframe_find(unsigned long ip, struct unwind_user_frame *frame) { return -ENOSYS; }
  static inline bool current_has_sframe(void) { return false; }
 
  #endif /* CONFIG_HAVE_UNWIND_USER_SFRAME */
kernel/unwind/sframe.c +218 -2
index fa7d87ffd00a..1a35615a361e 100644
@@ -15,9 +15,287 @@
  #include <linux/unwind_user_types.h>
 
  #include "sframe.h"
+#include "sframe_debug.h"
 
-#define dbg(fmt, ...)							\
-	pr_debug("%s (%d): " fmt, current->comm, current->pid, ##__VA_ARGS__)
+struct sframe_fre {
+	unsigned int	size;
+	s32		ip_off;
+	s32		cfa_off;
+	s32		ra_off;
+	s32		fp_off;
+	u8		info;
+};
+
+DEFINE_STATIC_SRCU(sframe_srcu);
+
+static __always_inline unsigned char fre_type_to_size(unsigned char fre_type)
+{
+	if (fre_type > 2)
+		return 0;
+	return 1 << fre_type;
+}
+
+static __always_inline unsigned char offset_size_enum_to_size(unsigned char off_size)
+{
+	if (off_size > 2)
+		return 0;
+	return 1 << off_size;
+}
+
+static __always_inline int __read_fde(struct sframe_section *sec,
+				      unsigned int fde_num,
+				      struct sframe_fde *fde)
+{
+	unsigned long fde_addr, ip;
+
+	fde_addr = sec->fdes_start + (fde_num * sizeof(struct sframe_fde));
+	unsafe_copy_from_user(fde, (void __user *)fde_addr,
+			      sizeof(struct sframe_fde), Efault);
+
+	ip = sec->sframe_start + fde->start_addr;
+	if (ip < sec->text_start || ip > sec->text_end)
+		return -EINVAL;
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+static __always_inline int __find_fde(struct sframe_section *sec,
+				      unsigned long ip,
+				      struct sframe_fde *fde)
+{
+	s32 ip_off, func_off_low = S32_MIN, func_off_high = S32_MAX;
+	struct sframe_fde __user *first, *low, *high, *found = NULL;
+	int ret;
+
+	ip_off = ip - sec->sframe_start;
+
+	first = (void __user *)sec->fdes_start;
+	low = first;
+	high = first + sec->num_fdes - 1;
+
+	while (low <= high) {
+		struct sframe_fde __user *mid;
+		s32 func_off;
+
+		mid = low + ((high - low) / 2);
+
+		unsafe_get_user(func_off, (s32 __user *)mid, Efault);
+
+		if (ip_off >= func_off) {
+			if (func_off < func_off_low)
+				return -EFAULT;
+
+			func_off_low = func_off;
+
+			found = mid;
+			low = mid + 1;
+		} else {
+			if (func_off > func_off_high)
+				return -EFAULT;
+
+			func_off_high = func_off;
+
+			high = mid - 1;
+		}
+	}
+
+	if (!found)
+		return -EINVAL;
+
+	ret = __read_fde(sec, found - first, fde);
+	if (ret)
+		return ret;
+
+	/* make sure it's not in a gap */
+	if (ip_off < fde->start_addr || ip_off >= fde->start_addr + fde->func_size)
+		return -EINVAL;
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+#define __UNSAFE_GET_USER_INC(to, from, type, label)			\
+({									\
+	type __to;							\
+	unsafe_get_user(__to, (type __user *)from, label);		\
+	from += sizeof(__to);						\
+	to = (typeof(to))__to;							\
+})
+
+#define UNSAFE_GET_USER_INC(to, from, size, label)			\
+({									\
+	switch (size) {							\
+	case 1:								\
+		__UNSAFE_GET_USER_INC(to, from, u8, label);		\
+		break;							\
+	case 2:								\
+		__UNSAFE_GET_USER_INC(to, from, u16, label);		\
+		break;							\
+	case 4:								\
+		__UNSAFE_GET_USER_INC(to, from, u32, label);		\
+		break;							\
+	default:							\
+		return -EFAULT;						\
+	}								\
+})
+
+static __always_inline int __read_fre(struct sframe_section *sec,
+				      struct sframe_fde *fde,
+				      unsigned long fre_addr,
+				      struct sframe_fre *fre)
+{
+	unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+	unsigned char fre_type = SFRAME_FUNC_FRE_TYPE(fde->info);
+	unsigned char offset_count, offset_size;
+	s32 ip_off, cfa_off, ra_off, fp_off;
+	unsigned long cur = fre_addr;
+	unsigned char addr_size;
+	u8 info;
+
+	addr_size = fre_type_to_size(fre_type);
+	if (!addr_size)
+		return -EFAULT;
+
+	if (fre_addr + addr_size + 1 > sec->fres_end)
+		return -EFAULT;
+
+	UNSAFE_GET_USER_INC(ip_off, cur, addr_size, Efault);
+	if (fde_type == SFRAME_FDE_TYPE_PCINC && ip_off > fde->func_size)
+		return -EFAULT;
+
+	UNSAFE_GET_USER_INC(info, cur, 1, Efault);
+	offset_count = SFRAME_FRE_OFFSET_COUNT(info);
+	offset_size  = offset_size_enum_to_size(SFRAME_FRE_OFFSET_SIZE(info));
+	if (!offset_count || !offset_size)
+		return -EFAULT;
+
+	if (cur + (offset_count * offset_size) > sec->fres_end)
+		return -EFAULT;
+
+	fre->size = addr_size + 1 + (offset_count * offset_size);
+
+	UNSAFE_GET_USER_INC(cfa_off, cur, offset_size, Efault);
+	offset_count--;
+
+	ra_off = sec->ra_off;
+	if (!ra_off) {
+		if (!offset_count--)
+			return -EFAULT;
+
+		UNSAFE_GET_USER_INC(ra_off, cur, offset_size, Efault);
+	}
+
+	fp_off = sec->fp_off;
+	if (!fp_off && offset_count) {
+		offset_count--;
+		UNSAFE_GET_USER_INC(fp_off, cur, offset_size, Efault);
+	}
+
+	if (offset_count)
+		return -EFAULT;
+
+	fre->ip_off		= ip_off;
+	fre->cfa_off		= cfa_off;
+	fre->ra_off		= ra_off;
+	fre->fp_off		= fp_off;
+	fre->info		= info;
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+static __always_inline int __find_fre(struct sframe_section *sec,
+				      struct sframe_fde *fde, unsigned long ip,
+				      struct unwind_user_frame *frame)
+{
+	unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+	struct sframe_fre *fre, *prev_fre = NULL;
+	struct sframe_fre fres[2];
+	unsigned long fre_addr;
+	bool which = false;
+	unsigned int i;
+	s32 ip_off;
+
+	ip_off = (s32)(ip - sec->sframe_start) - fde->start_addr;
+
+	if (fde_type == SFRAME_FDE_TYPE_PCMASK)
+		ip_off %= fde->rep_size;
+
+	fre_addr = sec->fres_start + fde->fres_off;
+
+	for (i = 0; i < fde->fres_num; i++) {
+		int ret;
+
+		/*

kernel/unwind/sframe.c line 246

Quoted 10 lines
kernel/unwind/sframe.c +10 -0
+		 * Alternate between the two fre_addr[] entries for 'fre' and
+		 * 'prev_fre'.
+		 */
+		fre = which ? fres : fres + 1;
+		which = !which;
+
+		ret = __read_fre(sec, fde, fre_addr, fre);
+		if (ret)
+			return ret;
+

It should be possible to only read the ip_off and info from FRE and defer the reading of offsets (as done in __read_fre) until later when you do need the offsets. See below.

We can find the relevant FRE with the following pieces of information:
- ip_off
- fre_size (this will mean we need to read the uin8_t info in the FRE)

Quoted 5 lines
kernel/unwind/sframe.c +5 -0
+		fre_addr += fre->size;
+
+		if (prev_fre && fre->ip_off <= prev_fre->ip_off)
+			return -EFAULT;
+

kernel/unwind/sframe.c line 261

Quoted 10 lines
kernel/unwind/sframe.c +10 -0
+		if (fre->ip_off > ip_off)
+			break;
+
+		prev_fre = fre;
+	}
+
+	if (!prev_fre)
+		return -EINVAL;
+	fre = prev_fre;
+

(Invoke the __read_fre here as we know now that this FRE is what we are looking for.)

Quoted 7 lines
kernel/unwind/sframe.c +7 -0
+	frame->cfa_off = fre->cfa_off;
+	frame->ra_off  = fre->ra_off;
+	frame->fp_off  = fre->fp_off;
+	frame->use_fp  = SFRAME_FRE_CFA_BASE_REG_ID(fre->info) == SFRAME_BASE_REG_FP;
+
+	return 0;
+}
author maintainer
#85 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 20 lines · Indu Bhagat
Quoted 4 lines · Josh Poimboeuf
kernel/unwind/sframe.c +4 -0
+	for (i = 0; i < fde->fres_num; i++) {
+		int ret;
+
+		/*

kernel/unwind/sframe.c line 246

Quoted 10 lines
kernel/unwind/sframe.c +10 -0
+		 * Alternate between the two fre_addr[] entries for 'fre' and
+		 * 'prev_fre'.
+		 */
+		fre = which ? fres : fres + 1;
+		which = !which;
+
+		ret = __read_fre(sec, fde, fre_addr, fre);
+		if (ret)
+			return ret;
+

It should be possible to only read the ip_off and info from FRE and defer the reading of offsets (as done in __read_fre) until later when you do need the offsets. See below.

Quoted 3 lines

We can find the relevant FRE with the following pieces of information:
- ip_off
- fre_size (this will mean we need to read the uin8_t info in the FRE)

Indeed, I'll skip reading the offsets until after the loop.

Signature · 1 line
Josh
#86 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

In reply to Josh Poimboeuf
Quoted 12 lines · Josh Poimboeuf

In preparation for using sframe to unwind user space stacks, add an sframe_find() interface for finding the sframe information associated with a given text address.

For performance, use user_read_access_begin() and the corresponding unsafe_*() accessors. Note that use of pr_debug() in uaccess-enabled regions would break noinstr validation, so there aren't any debug messages yet. That will be added in a subsequent commit.

kernel/unwind/sframe.c

Quoted 4 lines
kernel/unwind/sframe.c +3 -0
 
+struct sframe_fre {
+	unsigned int	size;
+	s32		ip_off;

The IP offset (from function start) in the SFrame V2 FDE is unsigned:

u32 ip_off;

Quoted 5 lines
kernel/unwind/sframe.c +5 -0
+	s32		cfa_off;
+	s32		ra_off;
+	s32		fp_off;
+	u8		info;
+};

...

Quoted 14 lines
kernel/unwind/sframe.c +14 -0
+#define __UNSAFE_GET_USER_INC(to, from, type, label)			\
+({									\
+	type __to;							\
+	unsafe_get_user(__to, (type __user *)from, label);		\
+	from += sizeof(__to);						\
+	to = (typeof(to))__to;							\
+})
+
+#define UNSAFE_GET_USER_INC(to, from, size, label)			\
+({									\
+	switch (size) {							\
+	case 1:								\
+		__UNSAFE_GET_USER_INC(to, from, u8, label);		\
+		break;							\

kernel/unwind/sframe.c

Quoted 10 lines
kernel/unwind/sframe.c +10 -0
+	case 2:								\
+		__UNSAFE_GET_USER_INC(to, from, u16, label);		\
+		break;							\
+	case 4:								\
+		__UNSAFE_GET_USER_INC(to, from, u32, label);		\
+		break;							\
+	default:							\
+		return -EFAULT;						\
+	}								\
+})

This does not work for the signed SFrame fields, such as the FRE CFA, RA, and FP offsets, as it does not perform the required sign extension. One option would be to rename to UNSAFE_GET_USER_UNSIGNED_INC() and re-introduce UNSAFE_GET_USER_SIGNED_INC() using s8, s16, and s32.

kernel/unwind/sframe.c

Quoted 9 lines
kernel/unwind/sframe.c +9 -0
+static __always_inline int __read_fre(struct sframe_section *sec,
+				      struct sframe_fde *fde,
+				      unsigned long fre_addr,
+				      struct sframe_fre *fre)
+{
+	unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+	unsigned char fre_type = SFRAME_FUNC_FRE_TYPE(fde->info);
+	unsigned char offset_count, offset_size;
+	s32 ip_off, cfa_off, ra_off, fp_off;

The IP offset (from function start) in the SFrame V2 FRE is unsigned:

u32 ip_off;
s32 cfa_off, ra_off, fp_off;

kernel/unwind/sframe.c

Quoted 12 lines
kernel/unwind/sframe.c +12 -0
+	unsigned long cur = fre_addr;
+	unsigned char addr_size;
+	u8 info;
+
+	addr_size = fre_type_to_size(fre_type);
+	if (!addr_size)
+		return -EFAULT;
+
+	if (fre_addr + addr_size + 1 > sec->fres_end)
+		return -EFAULT;
+
+	UNSAFE_GET_USER_INC(ip_off, cur, addr_size, Efault);

Ok: The SFrame V2 FRE IP offset is unsigned u8, u16, or u32.

kernel/unwind/sframe.c

Quoted 4 lines
kernel/unwind/sframe.c +4 -0
+	if (fde_type == SFRAME_FDE_TYPE_PCINC && ip_off > fde->func_size)
+		return -EFAULT;
+
+	UNSAFE_GET_USER_INC(info, cur, 1, Efault);

Ok: The SFrame V2 FRE info word is one byte of data.

kernel/unwind/sframe.c

Quoted 11 lines
kernel/unwind/sframe.c +11 -0
+	offset_count = SFRAME_FRE_OFFSET_COUNT(info);
+	offset_size  = offset_size_enum_to_size(SFRAME_FRE_OFFSET_SIZE(info));
+	if (!offset_count || !offset_size)
+		return -EFAULT;
+
+	if (cur + (offset_count * offset_size) > sec->fres_end)
+		return -EFAULT;
+
+	fre->size = addr_size + 1 + (offset_count * offset_size);
+
+	UNSAFE_GET_USER_INC(cfa_off, cur, offset_size, Efault);

Issue: The SFrame V2 FRE CFA offset is signed s8, s16, or s32. Sign extension required when storing in s32.

kernel/unwind/sframe.c

Quoted 8 lines
kernel/unwind/sframe.c +8 -0
+	offset_count--;
+
+	ra_off = sec->ra_off;
+	if (!ra_off) {
+		if (!offset_count--)
+			return -EFAULT;
+
+		UNSAFE_GET_USER_INC(ra_off, cur, offset_size, Efault);

Issue: The SFrame V2 FRE RA offset is signed s8, s16, or s32. Sign extension required when storing in s32.

kernel/unwind/sframe.c

Quoted 6 lines
kernel/unwind/sframe.c +6 -0
+	}
+
+	fp_off = sec->fp_off;
+	if (!fp_off && offset_count) {
+		offset_count--;
+		UNSAFE_GET_USER_INC(fp_off, cur, offset_size, Efault);

Issue: The SFrame V2 FRE FP offset is signed s8, s16, or s32. Sign extension required when storing in s32.

Quoted 20 lines
kernel/unwind/sframe.c +20 -0
+	}
+
+	if (offset_count)
+		return -EFAULT;
+
+	fre->ip_off		= ip_off;
+	fre->cfa_off		= cfa_off;
+	fre->ra_off		= ra_off;
+	fre->fp_off		= fp_off;
+	fre->info		= info;
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+static __always_inline int __find_fre(struct sframe_section *sec,
+				      struct sframe_fde *fde, unsigned long ip,
+				      struct unwind_user_frame *frame)

kernel/unwind/sframe.c

Quoted 10 lines
kernel/unwind/sframe.c +10 -0
+{
+	unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
+	struct sframe_fre *fre, *prev_fre = NULL;
+	struct sframe_fre fres[2];
+	unsigned long fre_addr;
+	bool which = false;
+	unsigned int i;
+	s32 ip_off;
+
+	ip_off = (s32)(ip - sec->sframe_start) - fde->start_addr;

The IP offset (from function start) in the SFrame V2 FRE is unsigned. The function start address offset (from .sframe section begin) is signed. Therefore:

u32 ip_off;

ip_off = ip - (sec->sframe_start + fde->start_addr);

kernel/unwind/sframe.c

Quoted 3 lines
kernel/unwind/sframe.c +3 -0
+
+	if (fde_type == SFRAME_FDE_TYPE_PCMASK)
+		ip_off %= fde->rep_size;

Following is a patch with the suggested changes, that were required to make unwinding of user space using SFrame work on s390:

kernel/unwind/sframe.c +31 -11
index bba14c5fe0f5..ea2d491ea68f 100644
@@ -19,7 +19,7 @@
 
 struct sframe_fre {
 	unsigned int	size;
-	s32		ip_off;
+	u32		ip_off;
 	s32		cfa_off;
 	s32		ra_off;
 	s32		fp_off;
@@ -136,7 +136,26 @@ static __always_inline int __find_fde(struct sframe_section *sec,
 	to = (typeof(to))__to;							\
 })
 
-#define UNSAFE_GET_USER_INC(to, from, size, label)			\
+#define UNSAFE_GET_USER_SIGNED_INC(to, from, size, label)		\
+({									\
+	switch (size) {							\
+	case 1:								\
+		__UNSAFE_GET_USER_INC(to, from, s8, label);		\
+		break;							\
+	case 2:								\
+		__UNSAFE_GET_USER_INC(to, from, s16, label);		\
+		break;							\
+	case 4:								\
+		__UNSAFE_GET_USER_INC(to, from, s32, label);		\
+		break;							\
+	default:							\
+		dbg_sec_uaccess("%d: bad UNSAFE_GET_USER_SIGNED__INC size %u\n",\
+				__LINE__, size);			\
+		return -EFAULT;						\
+	}								\
+})
+
+#define UNSAFE_GET_USER_UNSIGNED_INC(to, from, size, label)		\
 ({									\
 	switch (size) {							\
 	case 1:								\
@@ -149,7 +168,7 @@ static __always_inline int __find_fde(struct sframe_section *sec,
 		__UNSAFE_GET_USER_INC(to, from, u32, label);		\
 		break;							\
 	default:							\
-		dbg_sec_uaccess("%d: bad UNSAFE_GET_USER_INC size %u\n",\
+		dbg_sec_uaccess("%d: bad UNSAFE_GET_USER_UNSIGNED_INC size %u\n",\
 				__LINE__, size);			\
 		return -EFAULT;						\
 	}								\
@@ -163,7 +182,8 @@ static __always_inline int __read_fre(struct sframe_section *sec,
 	unsigned char fde_type = SFRAME_FUNC_FDE_TYPE(fde->info);
 	unsigned char fre_type = SFRAME_FUNC_FRE_TYPE(fde->info);
 	unsigned char offset_count, offset_size;
-	s32 ip_off, cfa_off, ra_off, fp_off;
+	u32 ip_off;
+	s32 cfa_off, ra_off, fp_off;
 	unsigned long cur = fre_addr;
 	unsigned char addr_size;
 	u8 info;
@@ -179,14 +199,14 @@ static __always_inline int __read_fre(struct sframe_section *sec,
 		return -EFAULT;
 	}
 
-	UNSAFE_GET_USER_INC(ip_off, cur, addr_size, Efault);
+	UNSAFE_GET_USER_UNSIGNED_INC(ip_off, cur, addr_size, Efault);
 	if (fde_type == SFRAME_FDE_TYPE_PCINC && ip_off > fde->func_size) {
 		dbg_sec_uaccess("fre starts past end of function: ip_off=0x%x, func_size=0x%x\n",
 				ip_off, fde->func_size);
 		return -EFAULT;
 	}
 
-	UNSAFE_GET_USER_INC(info, cur, 1, Efault);
+	UNSAFE_GET_USER_UNSIGNED_INC(info, cur, 1, Efault);
 	offset_count = SFRAME_FRE_OFFSET_COUNT(info);
 	offset_size  = offset_size_enum_to_size(SFRAME_FRE_OFFSET_SIZE(info));
 	if (!offset_count || !offset_size) {
@@ -200,7 +220,7 @@ static __always_inline int __read_fre(struct sframe_section *sec,
 
 	fre->size = addr_size + 1 + (offset_count * offset_size);
 
-	UNSAFE_GET_USER_INC(cfa_off, cur, offset_size, Efault);
+	UNSAFE_GET_USER_SIGNED_INC(cfa_off, cur, offset_size, Efault);
 	offset_count--;
 
 	ra_off = sec->ra_off;
@@ -210,13 +230,13 @@ static __always_inline int __read_fre(struct sframe_section *sec,
 			return -EFAULT;
 		}
 
-		UNSAFE_GET_USER_INC(ra_off, cur, offset_size, Efault);
+		UNSAFE_GET_USER_SIGNED_INC(ra_off, cur, offset_size, Efault);
 	}
 
 	fp_off = sec->fp_off;
 	if (!fp_off && offset_count) {
 		offset_count--;
-		UNSAFE_GET_USER_INC(fp_off, cur, offset_size, Efault);
+		UNSAFE_GET_USER_SIGNED_INC(fp_off, cur, offset_size, Efault);
 	}
 
 	if (offset_count) {
@@ -247,9 +267,9 @@ static __always_inline int __find_fre(struct sframe_section *sec,
 	unsigned long fre_addr;
 	bool which = false;
 	unsigned int i;
-	s32 ip_off;
+	u32 ip_off;
 
-	ip_off = (s32)(ip - sec->sframe_start) - fde->start_addr;
+	ip_off = ip - (sec->sframe_start + fde->start_addr);
 
 	if (fde_type == SFRAME_FDE_TYPE_PCMASK)
 		ip_off %= fde->rep_size;

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#87 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 8 lines · Jens Remus

Review

Quoted 3 lines · Josh Poimboeuf
(quoted diff) +3 -0
+struct sframe_fre {
+	unsigned int	size;
+	s32		ip_off;

The IP offset (from function start) in the SFrame V2 FDE is unsigned:

u32 ip_off;

Indeed.

Quoted 21 lines
Quoted 21 lines
(quoted diff) +21 -0
+#define __UNSAFE_GET_USER_INC(to, from, type, label)			\
+({									\
+	type __to;							\
+	unsafe_get_user(__to, (type __user *)from, label);		\
+	from += sizeof(__to);						\
+	to = (typeof(to))__to;							\
+})
+
+#define UNSAFE_GET_USER_INC(to, from, size, label)			\
+({									\
+	switch (size) {							\
+	case 1:								\
+		__UNSAFE_GET_USER_INC(to, from, u8, label);		\
+		break;							\
+	case 2:								\
+		__UNSAFE_GET_USER_INC(to, from, u16, label);		\
+		break;							\
+	case 4:								\
+		__UNSAFE_GET_USER_INC(to, from, u32, label);		\
+		break;							\
+	default:							\
Quoted 8 lines

kernel/unwind/sframe.c static __always_inline int __find_fde(struct sframe_section *sec, · line 156

Quoted 3 lines
kernel/unwind/sframe.c +3 -0
+		return -EFAULT;						\
+	}								\
+})

This does not work for the signed SFrame fields, such as the FRE CFA, RA, and FP offsets, as it does not perform the required sign extension. One option would be to rename to UNSAFE_GET_USER_UNSIGNED_INC() and re-introduce UNSAFE_GET_USER_SIGNED_INC() using s8, s16, and s32.

See the following line in __UNSAFE_GET_USER_INC():

	to = (typeof(to))__to;

Does that not do the sign extension?

Signature · 1 line
Josh
#88 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 33 lines · Josh Poimboeuf
Quoted 23 lines · Jens Remus
Quoted 21 lines · Josh Poimboeuf
(quoted diff) +21 -0
+#define __UNSAFE_GET_USER_INC(to, from, type, label)			\
+({									\
+	type __to;							\
+	unsafe_get_user(__to, (type __user *)from, label);		\
+	from += sizeof(__to);						\
+	to = (typeof(to))__to;							\
+})
+
+#define UNSAFE_GET_USER_INC(to, from, size, label)			\
+({									\
+	switch (size) {							\
+	case 1:								\
+		__UNSAFE_GET_USER_INC(to, from, u8, label);		\
+		break;							\
+	case 2:								\
+		__UNSAFE_GET_USER_INC(to, from, u16, label);		\
+		break;							\
+	case 4:								\
+		__UNSAFE_GET_USER_INC(to, from, u32, label);		\
+		break;							\
+	default:							\
Quoted 8 lines

kernel/unwind/sframe.c static __always_inline int __find_fde(struct sframe_section *sec, · line 156

Quoted 3 lines
kernel/unwind/sframe.c +3 -0
+		return -EFAULT;						\
+	}								\
+})

This does not work for the signed SFrame fields, such as the FRE CFA, RA, and FP offsets, as it does not perform the required sign extension. One option would be to rename to UNSAFE_GET_USER_UNSIGNED_INC() and re-introduce UNSAFE_GET_USER_SIGNED_INC() using s8, s16, and s32.

Quoted 5 lines

See the following line in __UNSAFE_GET_USER_INC():

	to = (typeof(to))__to;

Does that not do the sign extension?

No. In practice with my proposed changes reverted and the following debugging code added:

@@ -293,6 +293,10 @@ static __always_inline int __find_fre(struct sframe_section *sec,

                return -EINVAL;
        fre = prev_fre;

+ dbg_sec_uaccess("fre: ip_off=%u, cfa_off=%d, ra_off=%d, fp_off=%d, use_fp=%s, sp_val_off=%d\n",

+                       fre->ip_off, fre->cfa_off, fre->ra_off, fre->fp_off,
+                       SFRAME_FRE_CFA_BASE_REG_ID(fre->info) == SFRAME_BASE_REG_FP ? "y" : "n",
+                       sframe_sp_val_off());

Excerpt from dmesg:

sframe: /usr/lib/ld64.so.1: fre: ip_off=16, cfa_off=440, ra_off=208, fp_off=184, use_fp=n, sp_val_off=-160
sframe: /usr/lib/ld64.so.1: fre: ip_off=2600, cfa_off=672, ra_off=208, fp_off=184, use_fp=y, sp_val_off=-160
sframe: /usr/lib/ld64.so.1: fre: ip_off=10, cfa_off=368, ra_off=0, fp_off=0, use_fp=n, sp_val_off=-160
sframe: /usr/lib/ld64.so.1: fre: ip_off=722, cfa_off=672, ra_off=208, fp_off=184, use_fp=y, sp_val_off=-160

On s390 the register save slots have negative offsets from CFA (due to the CFA to be defined as SP at call site + 160). The RA, if saved, would be saved at CFA-48 on the stack. I.e. ra_off=-48 instead of ra_off=208 would have been correct.

208 = 0xd0 (unsigned) = -48 (signed)

Looking at the code:

UNSAFE_GET_USER_INC(ra_off, cur, offset_size, Efault);

With offset_size=1 expands into:

__UNSAFE_GET_USER_INC(/*to=*/ra_off, /*from=*cur, /*type=*/u8, /*label=*/Efault);

Expands into:

{
	u8 __to;
	unsafe_get_user(__to, (u8 __user *)cur, Efault);
	cur += sizeof(__to);
	ra_off = (typeof(ra_off))__to;
}

The issue is that on the last line __to is u8 instead of s8 and thus u8 gets casted to s32, which is performed without sign extension. __to would need to be s8 or get casted to s8 for sign extension to take place.

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#89 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 15 lines · Jens Remus

UNSAFE_GET_USER_INC(ra_off, cur, offset_size, Efault);

With offset_size=1 expands into:

__UNSAFE_GET_USER_INC(/*to=*/ra_off, /*from=*cur, /*type=*/u8, /*label=*/Efault);

Expands into:

{
	u8 __to;
	unsafe_get_user(__to, (u8 __user *)cur, Efault);
	cur += sizeof(__to);
	ra_off = (typeof(ra_off))__to;
}
Quoted 4 lines

The issue is that on the last line __to is u8 instead of s8 and thus u8 gets casted to s32, which is performed without sign extension. __to would need to be s8 or get casted to s8 for sign extension to take place.

Ah, I get it now, thanks for spelling that out for me.

Here's what I have at the moment:

#define __UNSAFE_GET_USER_INC(to, from, type, label) \

({									\
	type __to;							\
	unsafe_get_user(__to, (type __user *)from, label);		\
	from += sizeof(__to);						\
	to = __to;							\
})

#define __UNSAFE_GET_USER_INC(to, from, size, label, u_or_s) \

({									\
	switch (size) {							\
	case 1:								\
		__UNSAFE_GET_USER_INC(to, from, u_or_s##8, label);	\
		break;							\
	case 2:								\
		__UNSAFE_GET_USER_INC(to, from, u_or_s##16, label);	\
		break;							\
	case 4:								\
		__UNSAFE_GET_USER_INC(to, from, u_or_s##32, label);	\
		break;							\
	default:							\
		dbg_sec_uaccess("%d: bad unsafe_get_user() size %u\n",	\
				__LINE__, size);			\
		return -EFAULT;						\
	}								\
})

#define UNSAFE_GET_USER_UNSIGNED_INC(to, from, size, label) \
__UNSAFE_GET_USER_INC(to, from, size, label, u)

#define UNSAFE_GET_USER_SIGNED_INC(to, from, size, label) \
__UNSAFE_GET_USER_INC(to, from, size, label, s)

#define UNSAFE_GET_USER_INC(to, from, size, label)				\
	_Generic(to,								\
		 u8:	UNSAFE_GET_USER_UNSIGNED_INC(to, from, size, label),	\
		 u16:	UNSAFE_GET_USER_UNSIGNED_INC(to, from, size, label),	\
		 u32:	UNSAFE_GET_USER_UNSIGNED_INC(to, from, size, label),	\
		 s8:	UNSAFE_GET_USER_SIGNED_INC(to, from, size, label),	\
		 s16:	UNSAFE_GET_USER_SIGNED_INC(to, from, size, label),	\
		 s32:	UNSAFE_GET_USER_SIGNED_INC(to, from, size, label))
#90 lore raw

Re: [PATCH v4 19/39] unwind_user/sframe: Add support for reading .sframe contents

Quoted 16 lines · Josh Poimboeuf
Quoted 15 lines · Jens Remus

UNSAFE_GET_USER_INC(ra_off, cur, offset_size, Efault);

With offset_size=1 expands into:

__UNSAFE_GET_USER_INC(/*to=*/ra_off, /*from=*cur, /*type=*/u8, /*label=*/Efault);

Expands into:

{
	u8 __to;
	unsafe_get_user(__to, (u8 __user *)cur, Efault);
	cur += sizeof(__to);
	ra_off = (typeof(ra_off))__to;
}
Quoted 8 lines
Quoted 4 lines

The issue is that on the last line __to is u8 instead of s8 and thus u8 gets casted to s32, which is performed without sign extension. __to would need to be s8 or get casted to s8 for sign extension to take place.

Ah, I get it now, thanks for spelling that out for me.

Here's what I have at the moment:

Thanks! Using your new UNSAFE_GET_USER_INC() in all places works great on s390 when resolving the duplicate macro names (see below).

Quoted 10 lines

#define __UNSAFE_GET_USER_INC(to, from, type, label) \

({									\
	type __to;							\
	unsafe_get_user(__to, (type __user *)from, label);		\
	from += sizeof(__to);						\
	to = __to;							\
})

#define __UNSAFE_GET_USER_INC(to, from, size, label, u_or_s) \

That does not compile. One of the macros needs to be renamed.

CC kernel/unwind/sframe.o
kernel/unwind/sframe.c:141:9: warning: "__UNSAFE_GET_USER_INC" redefined

  141 | #define __UNSAFE_GET_USER_INC(to, from, size, label, u_or_s)            \
      |         ^~~~~~~~~~~~~~~~~~~~~
kernel/unwind/sframe.c:133:9: note: this is the location of the previous definition
  133 | #define __UNSAFE_GET_USER_INC(to, from, type, label)                    \
      |         ^~~~~~~~~~~~~~~~~~~~~
Quoted 24 lines
({									\
	switch (size) {							\
	case 1:								\
		__UNSAFE_GET_USER_INC(to, from, u_or_s##8, label);	\
		break;							\
	case 2:								\
		__UNSAFE_GET_USER_INC(to, from, u_or_s##16, label);	\
		break;							\
	case 4:								\
		__UNSAFE_GET_USER_INC(to, from, u_or_s##32, label);	\
		break;							\
	default:							\
		dbg_sec_uaccess("%d: bad unsafe_get_user() size %u\n",	\
				__LINE__, size);			\
		return -EFAULT;						\
	}								\
})

#define UNSAFE_GET_USER_UNSIGNED_INC(to, from, size, label) \
__UNSAFE_GET_USER_INC(to, from, size, label, u)

#define UNSAFE_GET_USER_SIGNED_INC(to, from, size, label) \
__UNSAFE_GET_USER_INC(to, from, size, label, s)

Quoted 8 lines
#define UNSAFE_GET_USER_INC(to, from, size, label)				\
	_Generic(to,								\
		 u8:	UNSAFE_GET_USER_UNSIGNED_INC(to, from, size, label),	\
		 u16:	UNSAFE_GET_USER_UNSIGNED_INC(to, from, size, label),	\
		 u32:	UNSAFE_GET_USER_UNSIGNED_INC(to, from, size, label),	\
		 s8:	UNSAFE_GET_USER_SIGNED_INC(to, from, size, label),	\
		 s16:	UNSAFE_GET_USER_SIGNED_INC(to, from, size, label),	\
		 s32:	UNSAFE_GET_USER_SIGNED_INC(to, from, size, label))

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#91 lore raw

PATCH v4 20/39 unwind_user/sframe: Detect .sframe sections in executables

In reply to Josh Poimboeuf

When loading an ELF executable, automatically detect an .sframe section and associate it with the mm_struct.

---
 fs/binfmt_elf.c          | 49 +++++++++++++++++++++++++++++++++++++---
 include/uapi/linux/elf.h |  1 +
 2 files changed, 47 insertions(+), 3 deletions(-)
fs/binfmt_elf.c +46 -3
index 106f0e8af177..90cd745e5bd6 100644
@@ -47,6 +47,7 @@
 #include <linux/dax.h>
 #include <linux/uaccess.h>
 #include <linux/rseq.h>
+#include <linux/sframe.h>
 #include <asm/param.h>
 #include <asm/page.h>
 
@@ -629,6 +630,21 @@ static inline int make_prot(u32 p_flags, struct arch_elf_state *arch_state,
 	return arch_elf_adjust_prot(prot, arch_state, has_interp, is_interp);
 }
 
+static void elf_add_sframe(struct elf_phdr *text, struct elf_phdr *sframe,
+			   unsigned long base_addr)
+{
+	unsigned long sframe_start, sframe_end, text_start, text_end;
+
+	sframe_start = base_addr + sframe->p_vaddr;
+	sframe_end   = sframe_start + sframe->p_memsz;
+
+	text_start   = base_addr + text->p_vaddr;
+	text_end     = text_start + text->p_memsz;
+
+	/* Ignore return value, sframe section isn't critical */
+	sframe_add_section(sframe_start, sframe_end, text_start, text_end);
+}
+
 /* This is much more generalized than the library routine read function,
    so we keep this separate.  Technically the library read function
    is only provided so that we can read a.out libraries that have
@@ -639,7 +655,7 @@ static unsigned long load_elf_interp(struct elfhdr *interp_elf_ex,
 		unsigned long no_base, struct elf_phdr *interp_elf_phdata,
 		struct arch_elf_state *arch_state)
 {
-	struct elf_phdr *eppnt;
+	struct elf_phdr *eppnt, *sframe_phdr = NULL;
 	unsigned long load_addr = 0;
 	int load_addr_set = 0;
 	unsigned long error = ~0UL;
@@ -665,7 +681,8 @@ static unsigned long load_elf_interp(struct elfhdr *interp_elf_ex,
 
 	eppnt = interp_elf_phdata;
 	for (i = 0; i < interp_elf_ex->e_phnum; i++, eppnt++) {
-		if (eppnt->p_type == PT_LOAD) {
+		switch (eppnt->p_type) {
+		case PT_LOAD: {
 			int elf_type = MAP_PRIVATE;
 			int elf_prot = make_prot(eppnt->p_flags, arch_state,
 						 true, true);
@@ -704,6 +721,20 @@ static unsigned long load_elf_interp(struct elfhdr *interp_elf_ex,
 				error = -ENOMEM;
 				goto out;
 			}
+			break;
+		}
+		case PT_GNU_SFRAME:
+			sframe_phdr = eppnt;
+			break;
+		}
+	}
+
+	if (sframe_phdr) {
+		eppnt = interp_elf_phdata;
+		for (i = 0; i < interp_elf_ex->e_phnum; i++, eppnt++) {
+			if (eppnt->p_flags & PF_X) {
+				elf_add_sframe(eppnt, sframe_phdr, load_addr);
+			}
 		}
 	}
 
@@ -829,7 +860,7 @@ static int load_elf_binary(struct linux_binprm *bprm)
 	int first_pt_load = 1;
 	unsigned long error;
 	struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
-	struct elf_phdr *elf_property_phdata = NULL;
+	struct elf_phdr *elf_property_phdata = NULL, *sframe_phdr = NULL;
 	unsigned long elf_brk;
 	int retval, i;
 	unsigned long elf_entry;
@@ -937,6 +968,10 @@ static int load_elf_binary(struct linux_binprm *bprm)
 				executable_stack = EXSTACK_DISABLE_X;
 			break;
 
+		case PT_GNU_SFRAME:
+			sframe_phdr = elf_ppnt;
+			break;
+
 		case PT_LOPROC ... PT_HIPROC:
 			retval = arch_elf_pt_proc(elf_ex, elf_ppnt,
 						  bprm->file, false,
@@ -1227,6 +1262,14 @@ static int load_elf_binary(struct linux_binprm *bprm)
 			elf_brk = k;
 	}
 
+	if (sframe_phdr) {
+		for (i = 0, elf_ppnt = elf_phdata;
+		     i < elf_ex->e_phnum; i++, elf_ppnt++) {
+			if ((elf_ppnt->p_flags & PF_X))
+				elf_add_sframe(elf_ppnt, sframe_phdr, load_bias);
+		}
+	}
+
 	e_entry = elf_ex->e_entry + load_bias;
 	phdr_addr += load_bias;
 	elf_brk += load_bias;
include/uapi/linux/elf.h +1 -0
index b44069d29cec..026978cddc2e 100644
@@ -39,6 +39,7 @@ typedef __s64	Elf64_Sxword;
 #define PT_GNU_STACK	(PT_LOOS + 0x474e551)
 #define PT_GNU_RELRO	(PT_LOOS + 0x474e552)
 #define PT_GNU_PROPERTY	(PT_LOOS + 0x474e553)
+#define PT_GNU_SFRAME	(PT_LOOS + 0x474e554)
 
 
 /* ARM MTE memory tag segment type */
Signature · 1 line
2.48.1
author maintainer
#92 lore raw

PATCH v4 21/39 unwind_user/sframe: Add prctl() interface for registering .sframe sections

In reply to Josh Poimboeuf

The kernel doesn't have direct visibility to the ELF contents of shared libraries. Add some prctl() interfaces which allow glibc to tell the kernel where to find .sframe sections.

---
 include/uapi/linux/prctl.h | 5 ++++-
 kernel/sys.c               | 9 +++++++++
 2 files changed, 13 insertions(+), 1 deletion(-)
include/uapi/linux/prctl.h +4 -1
index 5c6080680cb2..4a52e3f9ccc9 100644
@@ -351,6 +351,9 @@ struct prctl_mm_map {
  * configuration.  All bits may be locked via this call, including
  * undefined bits.
  */
-#define PR_LOCK_SHADOW_STACK_STATUS      76
+#define PR_LOCK_SHADOW_STACK_STATUS	76
+
+#define PR_ADD_SFRAME			77
+#define PR_REMOVE_SFRAME		78
 
 #endif /* _LINUX_PRCTL_H */
kernel/sys.c +9 -0
index c4c701c6f0b4..414dfd6ee9fa 100644
@@ -64,6 +64,7 @@
 #include <linux/rcupdate.h>
 #include <linux/uidgid.h>
 #include <linux/cred.h>
+#include <linux/sframe.h>
 
 #include <linux/nospec.h>
 
@@ -2809,6 +2810,14 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
 			return -EINVAL;
 		error = arch_lock_shadow_stack_status(me, arg2);
 		break;
+	case PR_ADD_SFRAME:
+		error = sframe_add_section(arg2, arg3, arg4, arg5);
+		break;
+	case PR_REMOVE_SFRAME:
+		if (arg3 || arg4 || arg5)
+			return -EINVAL;
+		error = sframe_remove_section(arg2);
+		break;
 	default:
 		error = -EINVAL;
 		break;
Signature · 1 line
2.48.1
author maintainer
#93 lore raw

PATCH v4 22/39 unwind_user/sframe: Wire up unwind_user to sframe

In reply to Josh Poimboeuf

Now that the sframe infrastructure is fully in place, make it work by hooking it up to the unwind_user interface.

---
 arch/Kconfig                      |  1 +
 include/linux/unwind_user_types.h |  1 +
 kernel/unwind/user.c              | 22 +++++++++++++++++++---
 3 files changed, 21 insertions(+), 3 deletions(-)
arch/Kconfig +1 -0
index 23edd0e4e16a..12a3b73cbe66 100644
@@ -448,6 +448,7 @@ config HAVE_UNWIND_USER_COMPAT_FP
 
 config HAVE_UNWIND_USER_SFRAME
 	bool
+	select UNWIND_USER
 
 config AS_SFRAME
 	def_bool $(as-instr,.cfi_sections .sframe\n.cfi_startproc\n.cfi_endproc)
include/linux/unwind_user_types.h +1 -0
index 3ec4a097a3dd..5558558948b7 100644
@@ -9,6 +9,7 @@ enum unwind_user_type {
 	UNWIND_USER_TYPE_NONE,
 	UNWIND_USER_TYPE_FP,
 	UNWIND_USER_TYPE_COMPAT_FP,
+	UNWIND_USER_TYPE_SFRAME,
 };
 
 struct unwind_stacktrace {
kernel/unwind/user.c +19 -3
index 92963f129c6a..fc0c75da81f6 100644
@@ -6,6 +6,7 @@
 #include <linux/sched.h>
 #include <linux/sched/task_stack.h>
 #include <linux/unwind_user.h>
+#include <linux/sframe.h>
 #include <linux/uaccess.h>
 #include <asm/unwind_user.h>
 
@@ -29,6 +30,12 @@ static inline bool compat_state(struct unwind_user_state *state)
 	       state->type == UNWIND_USER_TYPE_COMPAT_FP;
 }
 
+static inline bool sframe_state(struct unwind_user_state *state)
+{
+	return IS_ENABLED(CONFIG_HAVE_UNWIND_USER_SFRAME) &&
+	       state->type == UNWIND_USER_TYPE_SFRAME;
+}
+
 #define UNWIND_GET_USER_LONG(to, from, state)				\
 ({									\
 	int __ret;							\
@@ -48,12 +55,19 @@ int unwind_user_next(struct unwind_user_state *state)
 	if (state->done)
 		return -EINVAL;
 
-	if (compat_state(state))
+	if (compat_state(state)) {
 		frame = &compat_fp_frame;
-	else if (fp_state(state))
+	} else if (sframe_state(state)) {
+		if (sframe_find(state->ip, frame)) {
+			if (!IS_ENABLED(CONFIG_HAVE_UNWIND_USER_FP))
+				goto the_end;
+			frame = &fp_frame;
+		}
+	} else if (fp_state(state)) {
 		frame = &fp_frame;
-	else
+	} else {
 		goto the_end;
+	}
 
 	cfa = (frame->use_fp ? state->fp : state->sp) + frame->cfa_off;
 
@@ -94,6 +108,8 @@ int unwind_user_start(struct unwind_user_state *state)
 
 	if (IS_ENABLED(CONFIG_HAVE_UNWIND_USER_COMPAT_FP) && in_compat_mode(regs))
 		state->type = UNWIND_USER_TYPE_COMPAT_FP;
+	else if (current_has_sframe())
+		state->type = UNWIND_USER_TYPE_SFRAME;
 	else if (IS_ENABLED(CONFIG_HAVE_UNWIND_USER_FP))
 		state->type = UNWIND_USER_TYPE_FP;
 	else
Signature · 1 line
2.48.1
author maintainer
#94 lore raw

PATCH v4 23/39 unwind_user/sframe/x86: Enable sframe unwinding on x86

In reply to Josh Poimboeuf

The x86 sframe 2.0 implementation works fairly well, starting with binutils 2.41 (though some bugs are getting fixed in later versions). Enable it.

---
 arch/x86/Kconfig | 1 +
 1 file changed, 1 insertion(+)
arch/x86/Kconfig +1 -0
index 08c44db0fefb..1016f8f80447 100644
@@ -293,6 +293,7 @@ config X86
 	select HAVE_UNSTABLE_SCHED_CLOCK
 	select HAVE_UNWIND_USER_COMPAT_FP	if IA32_EMULATION
 	select HAVE_UNWIND_USER_FP		if X86_64
+	select HAVE_UNWIND_USER_SFRAME		if X86_64
 	select HAVE_USER_RETURN_NOTIFIER
 	select HAVE_GENERIC_VDSO
 	select VDSO_GETRANDOM			if X86_64
Signature · 1 line
2.48.1
author maintainer
#95 lore raw

PATCH v4 24/39 unwind_user/sframe: Remove .sframe section on detected corruption

In reply to Josh Poimboeuf

To avoid continued attempted use of a bad .sframe section, remove it on demand when the first sign of corruption is detected.

---
 kernel/unwind/sframe.c | 4 ++++
 1 file changed, 4 insertions(+)
kernel/unwind/sframe.c +4 -0
index 1a35615a361e..66b920441692 100644
@@ -294,6 +294,10 @@ int sframe_find(unsigned long ip, struct unwind_user_frame *frame)
 	ret = __find_fre(sec, &fde, ip, frame);
 end:
 	user_read_access_end();
+
+	if (ret == -EFAULT)
+		WARN_ON_ONCE(sframe_remove_section(sec->sframe_start));
+
 	return ret;
 }
 
Signature · 1 line
2.48.1
author maintainer
#96 lore raw

PATCH v4 25/39 unwind_user/sframe: Show file name in debug output

In reply to Josh Poimboeuf

When debugging sframe issues, the error messages aren't all that helpful without knowing what file a corresponding .sframe section belongs to. Prefix debug output strings with the file name.

---
 include/linux/sframe.h       |  4 +++-
 kernel/unwind/sframe.c       | 23 ++++++++++++--------
 kernel/unwind/sframe_debug.h | 41 ++++++++++++++++++++++++++++++------
 3 files changed, 52 insertions(+), 16 deletions(-)
include/linux/sframe.h +3 -1
index 2e70085a1e89..18a04d574090 100644
@@ -10,7 +10,9 @@
 
 struct sframe_section {
 	struct rcu_head	rcu;
-
+#ifdef CONFIG_DYNAMIC_DEBUG
+	const char	*filename;
+#endif
 	unsigned long	sframe_start;
 	unsigned long	sframe_end;
 	unsigned long	text_start;
kernel/unwind/sframe.c +14 -9
index 66b920441692..f463123f9afe 100644
@@ -295,14 +295,17 @@ int sframe_find(unsigned long ip, struct unwind_user_frame *frame)
 end:
 	user_read_access_end();
 
-	if (ret == -EFAULT)
+	if (ret == -EFAULT) {
+		dbg_sec("removing bad .sframe section\n");
 		WARN_ON_ONCE(sframe_remove_section(sec->sframe_start));
+	}
 
 	return ret;
 }
 
 static void free_section(struct sframe_section *sec)
 {
+	dbg_free(sec);
 	kfree(sec);
 }
 
@@ -313,7 +316,7 @@ static int sframe_read_header(struct sframe_section *sec)
 	unsigned int num_fdes;
 
 	if (copy_from_user(&shdr, (void __user *)sec->sframe_start, sizeof(shdr))) {
-		dbg("header usercopy failed\n");
+		dbg_sec("header usercopy failed\n");
 		return -EFAULT;
 	}
 
@@ -321,18 +324,18 @@ static int sframe_read_header(struct sframe_section *sec)
 	    shdr.preamble.version != SFRAME_VERSION_2 ||
 	    !(shdr.preamble.flags & SFRAME_F_FDE_SORTED) ||
 	    shdr.auxhdr_len) {
-		dbg("bad/unsupported sframe header\n");
+		dbg_sec("bad/unsupported sframe header\n");
 		return -EINVAL;
 	}
 
 	if (!shdr.num_fdes || !shdr.num_fres) {
-		dbg("no fde/fre entries\n");
+		dbg_sec("no fde/fre entries\n");
 		return -EINVAL;
 	}
 
 	header_end = sec->sframe_start + SFRAME_HEADER_SIZE(shdr);
 	if (header_end >= sec->sframe_end) {
-		dbg("header doesn't fit in section\n");
+		dbg_sec("header doesn't fit in section\n");
 		return -EINVAL;
 	}
 
@@ -344,7 +347,7 @@ static int sframe_read_header(struct sframe_section *sec)
 	fres_end   = fres_start + shdr.fre_len;
 
 	if (fres_start < fdes_end || fres_end > sec->sframe_end) {
-		dbg("inconsistent fde/fre offsets\n");
+		dbg_sec("inconsistent fde/fre offsets\n");
 		return -EINVAL;
 	}
 
@@ -400,6 +403,8 @@ int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end,
 	sec->text_start		= text_start;
 	sec->text_end		= text_end;
 
+	dbg_init(sec);
+
 	ret = sframe_read_header(sec);
 	if (ret) {
 		dbg_print_header(sec);
@@ -408,8 +413,8 @@ int sframe_add_section(unsigned long sframe_start, unsigned long sframe_end,
 
 	ret = mtree_insert_range(sframe_mt, sec->text_start, sec->text_end, sec, GFP_KERNEL);
 	if (ret) {
-		dbg("mtree_insert_range failed: text=%lx-%lx\n",
-		    sec->text_start, sec->text_end);
+		dbg_sec("mtree_insert_range failed: text=%lx-%lx\n",
+			sec->text_start, sec->text_end);
 		goto err_free;
 	}
 
@@ -431,7 +436,7 @@ static int __sframe_remove_section(struct mm_struct *mm,
 				   struct sframe_section *sec)
 {
 	if (!mtree_erase(&mm->sframe_mt, sec->text_start)) {
-		dbg("mtree_erase failed: text=%lx\n", sec->text_start);
+		dbg_sec("mtree_erase failed: text=%lx\n", sec->text_start);
 		return -EINVAL;
 	}
 
kernel/unwind/sframe_debug.h +35 -6

61 lines not on this page · raw message

Signature · 1 line
2.48.1
#97 lore raw

Re: [PATCH v4 25/39] unwind_user/sframe: Show file name in debug output

Quoted 10 lines · Josh Poimboeuf

When debugging sframe issues, the error messages aren't all that helpful without knowing what file a corresponding .sframe section belongs to. Prefix debug output strings with the file name.

kernel/unwind/sframe_debug.h +2 -0
 
+static inline void dbg_init(struct sframe_section *sec)
+{

kernel/unwind/sframe_debug.h line 41

Quoted 10 lines
kernel/unwind/sframe_debug.h +10 -0
+	struct mm_struct *mm = current->mm;
+	struct vm_area_struct *vma;
+
+	guard(mmap_read_lock)(mm);
+	vma = vma_lookup(mm, sec->sframe_start);
+	if (!vma)
+		sec->filename = kstrdup("(vma gone???)", GFP_KERNEL);
+	else if (vma->vm_file)
+		sec->filename = kstrdup_quotable_file(vma->vm_file, GFP_KERNEL);
+	else if (!vma->vm_mm)

This condition does not appear to work for vdso on s390. The following does:

	else if (in_range(sec->sframe_start, current->mm->context.vdso_base, vdso_text_size()))

kernel/unwind/sframe_debug.h line 45

Quoted 4 lines
kernel/unwind/sframe_debug.h +4 -0
+		sec->filename = kstrdup("(vdso)", GFP_KERNEL);
+	else
+		sec->filename = kstrdup("(anonymous)", GFP_KERNEL);
+}

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#98 lore raw

Re: [PATCH v4 25/39] unwind_user/sframe: Show file name in debug output

Quoted 9 lines · Jens Remus
Quoted 7 lines · Josh Poimboeuf

When debugging sframe issues, the error messages aren't all that helpful without knowing what file a corresponding .sframe section belongs to. Prefix debug output strings with the file name.

kernel/unwind/sframe_debug.h +0 -0
Quoted 14 lines

kernel/unwind/sframe_debug.h

Quoted 12 lines
kernel/unwind/sframe_debug.h +12 -0
+static inline void dbg_init(struct sframe_section *sec)
+{
+	struct mm_struct *mm = current->mm;
+	struct vm_area_struct *vma;
+
+	guard(mmap_read_lock)(mm);
+	vma = vma_lookup(mm, sec->sframe_start);
+	if (!vma)
+		sec->filename = kstrdup("(vma gone???)", GFP_KERNEL);
+	else if (vma->vm_file)
+		sec->filename = kstrdup_quotable_file(vma->vm_file, GFP_KERNEL);
+	else if (!vma->vm_mm)

This condition does not appear to work for vdso on s390.

I had a fix for this which somehow got dropped:

kernel/unwind/sframe_debug.h +4 -0
index 3bb3c5574aee..045e9c0b16c9 100644
@@ -67,6 +67,10 @@ static inline void dbg_init(struct sframe_section *sec)
 		sec->filename = kstrdup("(vma gone???)", GFP_KERNEL);
 	else if (vma->vm_file)
 		sec->filename = kstrdup_quotable_file(vma->vm_file, GFP_KERNEL);
+	else if (vma->vm_ops && vma->vm_ops->name)
+		sec->filename = kstrdup(vma->vm_ops->name(vma), GFP_KERNEL);
+	else if (arch_vma_name(vma))
+		sec->filename = kstrdup(arch_vma_name(vma), GFP_KERNEL);
 	else if (!vma->vm_mm)
 		sec->filename = kstrdup("(vdso)", GFP_KERNEL);
 	else
#99 lore raw

Re: [PATCH v4 25/39] unwind_user/sframe: Show file name in debug output

Quoted 27 lines · Josh Poimboeuf
Quoted 4 lines · Jens Remus
Quoted 1 line · Josh Poimboeuf
kernel/unwind/sframe_debug.h +0 -0
Quoted 14 lines

kernel/unwind/sframe_debug.h

Quoted 12 lines
kernel/unwind/sframe_debug.h +12 -0
+static inline void dbg_init(struct sframe_section *sec)
+{
+	struct mm_struct *mm = current->mm;
+	struct vm_area_struct *vma;
+
+	guard(mmap_read_lock)(mm);
+	vma = vma_lookup(mm, sec->sframe_start);
+	if (!vma)
+		sec->filename = kstrdup("(vma gone???)", GFP_KERNEL);
+	else if (vma->vm_file)
+		sec->filename = kstrdup_quotable_file(vma->vm_file, GFP_KERNEL);
+	else if (!vma->vm_mm)

This condition does not appear to work for vdso on s390.

I had a fix for this which somehow got dropped:

kernel/unwind/sframe_debug.h +0 -0
index 3bb3c5574aee..045e9c0b16c9 100644

kernel/unwind/sframe_debug.h static inline void dbg_init(struct sframe_section *sec) · line 76

Quoted 10 lines
kernel/unwind/sframe_debug.h +4 -0
@@ -67,6 +67,10 @@ static inline void dbg_init(struct sframe_section *sec)
  		sec->filename = kstrdup("(vma gone???)", GFP_KERNEL);
  	else if (vma->vm_file)
  		sec->filename = kstrdup_quotable_file(vma->vm_file, GFP_KERNEL);
+	else if (vma->vm_ops && vma->vm_ops->name)
+		sec->filename = kstrdup(vma->vm_ops->name(vma), GFP_KERNEL);
+	else if (arch_vma_name(vma))
+		sec->filename = kstrdup(arch_vma_name(vma), GFP_KERNEL);
  	else if (!vma->vm_mm)
  		sec->filename = kstrdup("(vdso)", GFP_KERNEL);
  	else

Thanks! Your patch works fine on s390:

test_vdso 1087 104.721004: 10000 task-clock:ppp:

             3ffd11f1702 __cvdso_clock_gettime_data.constprop.0+0x52 ([vdso])
             3ffd11f14c6 __kernel_clock_gettime+0x16 ([vdso])
             3ff8f6f113e clock_gettime@@GLIBC_2.17+0x1e (/usr/lib64/libc.so.6)
             3ff8f6e20e8 __gettimeofday+0x38 (/usr/lib64/libc.so.6)
                 100066e main+0x1e (/root/test/vdso/test_vdso)
             3ff8f63459c __libc_start_call_main+0x8c (/usr/lib64/libc.so.6)
             3ff8f63469e __libc_start_main@@GLIBC_2.34+0xae (/usr/lib64/libc.so.6)
                 100073a _start+0x3a (/root/test/vdso/test_vdso)

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#100 lore raw

PATCH v4 26/39 unwind_user/sframe: Enable debugging in uaccess regions

In reply to Josh Poimboeuf

Objtool warns about calling pr_debug() from uaccess-enabled regions, and rightfully so. Add a dbg_sec_uaccess() macro which temporarily disables uaccess before doing the dynamic printk, and use that to add debug messages throughout the uaccess-enabled regions.

---
 kernel/unwind/sframe.c       | 59 ++++++++++++++++++++++++++++--------
 kernel/unwind/sframe_debug.h | 31 +++++++++++++++++++
 2 files changed, 77 insertions(+), 13 deletions(-)
kernel/unwind/sframe.c +46 -13
index f463123f9afe..a2ca26b952d3 100644
@@ -53,12 +53,15 @@ static __always_inline int __read_fde(struct sframe_section *sec,
 			      sizeof(struct sframe_fde), Efault);
 
 	ip = sec->sframe_start + fde->start_addr;
-	if (ip < sec->text_start || ip > sec->text_end)
+	if (ip < sec->text_start || ip > sec->text_end) {
+		dbg_sec_uaccess("bad fde num %d\n", fde_num);
 		return -EINVAL;
+	}
 
 	return 0;
 
 Efault:
+	dbg_sec_uaccess("fde %d usercopy failed\n", fde_num);
 	return -EFAULT;
 }
 
@@ -85,16 +88,22 @@ static __always_inline int __find_fde(struct sframe_section *sec,
 		unsafe_get_user(func_off, (s32 __user *)mid, Efault);
 
 		if (ip_off >= func_off) {
-			if (func_off < func_off_low)
+			if (func_off < func_off_low) {
+				dbg_sec_uaccess("fde %u not sorted\n",
+						(unsigned int)(mid - first));
 				return -EFAULT;
+			}
 
 			func_off_low = func_off;
 
 			found = mid;
 			low = mid + 1;
 		} else {
-			if (func_off > func_off_high)
+			if (func_off > func_off_high) {
+				dbg_sec_uaccess("fde %u not sorted\n",
+						(unsigned int)(mid - first));
 				return -EFAULT;
+			}
 
 			func_off_high = func_off;
 
@@ -140,6 +149,8 @@ static __always_inline int __find_fde(struct sframe_section *sec,
 		__UNSAFE_GET_USER_INC(to, from, u32, label);		\
 		break;							\
 	default:							\
+		dbg_sec_uaccess("%d: bad UNSAFE_GET_USER_INC size %u\n",\
+				__LINE__, size);			\
 		return -EFAULT;						\
 	}								\
 })
@@ -158,24 +169,34 @@ static __always_inline int __read_fre(struct sframe_section *sec,
 	u8 info;
 
 	addr_size = fre_type_to_size(fre_type);
-	if (!addr_size)
+	if (!addr_size) {
+		dbg_sec_uaccess("bad addr_size in fde info %u\n", fde->info);
 		return -EFAULT;
+	}
 
-	if (fre_addr + addr_size + 1 > sec->fres_end)
+	if (fre_addr + addr_size + 1 > sec->fres_end) {
+		dbg_sec_uaccess("fre addr+info goes past end of subsection\n");
 		return -EFAULT;
+	}
 
 	UNSAFE_GET_USER_INC(ip_off, cur, addr_size, Efault);
-	if (fde_type == SFRAME_FDE_TYPE_PCINC && ip_off > fde->func_size)
+	if (fde_type == SFRAME_FDE_TYPE_PCINC && ip_off > fde->func_size) {
+		dbg_sec_uaccess("fre starts past end of function: ip_off=0x%x, func_size=0x%x\n",
+				ip_off, fde->func_size);
 		return -EFAULT;
+	}
 
 	UNSAFE_GET_USER_INC(info, cur, 1, Efault);
 	offset_count = SFRAME_FRE_OFFSET_COUNT(info);
 	offset_size  = offset_size_enum_to_size(SFRAME_FRE_OFFSET_SIZE(info));
-	if (!offset_count || !offset_size)
+	if (!offset_count || !offset_size) {
+		dbg_sec_uaccess("zero offset_count or size in fre info %u\n",info);
 		return -EFAULT;
-
-	if (cur + (offset_count * offset_size) > sec->fres_end)
+	}
+	if (cur + (offset_count * offset_size) > sec->fres_end) {
+		dbg_sec_uaccess("fre goes past end of subsection\n");
 		return -EFAULT;
+	}
 
 	fre->size = addr_size + 1 + (offset_count * offset_size);
 
@@ -184,8 +205,10 @@ static __always_inline int __read_fre(struct sframe_section *sec,
 
 	ra_off = sec->ra_off;
 	if (!ra_off) {
-		if (!offset_count--)
+		if (!offset_count--) {
+			dbg_sec_uaccess("zero offset_count, can't find ra_off\n");
 			return -EFAULT;
+		}
 
 		UNSAFE_GET_USER_INC(ra_off, cur, offset_size, Efault);
 	}
@@ -196,8 +219,10 @@ static __always_inline int __read_fre(struct sframe_section *sec,
 		UNSAFE_GET_USER_INC(fp_off, cur, offset_size, Efault);
 	}
 
-	if (offset_count)
+	if (offset_count) {
+		dbg_sec_uaccess("non-zero offset_count after reading fre\n");
 		return -EFAULT;
+	}
 
 	fre->ip_off		= ip_off;
 	fre->cfa_off		= cfa_off;
@@ -208,6 +233,7 @@ static __always_inline int __read_fre(struct sframe_section *sec,
 	return 0;
 
 Efault:
+	dbg_sec_uaccess("fre usercopy failed\n");
 	return -EFAULT;
 }
 
@@ -241,13 +267,20 @@ static __always_inline int __find_fre(struct sframe_section *sec,
 		which = !which;
 
 		ret = __read_fre(sec, fde, fre_addr, fre);
-		if (ret)
+		if (ret) {
+			dbg_sec_uaccess("fde addr 0x%x: __read_fre(%u) failed\n",
+					fde->start_addr, i);
+			dbg_print_fde_uaccess(sec, fde);
 			return ret;
+		}
 
 		fre_addr += fre->size;
 
-		if (prev_fre && fre->ip_off <= prev_fre->ip_off)
+		if (prev_fre && fre->ip_off <= prev_fre->ip_off) {
+			dbg_sec_uaccess("fde addr 0x%x: fre %u not sorted\n",
+					fde->start_addr, i);
 			return -EFAULT;
+		}
 
 		if (fre->ip_off > ip_off)
 			break;
kernel/unwind/sframe_debug.h +31 -0
index 4d121cdbb760..3bb3c5574aee 100644
@@ -13,6 +13,26 @@
 #define dbg_sec(fmt, ...)						\
 	dbg("%s: " fmt, sec->filename, ##__VA_ARGS__)
 
+#define __dbg_sec_descriptor(fmt, ...)					\
+	__dynamic_pr_debug(&descriptor, "sframe: %s: " fmt,		\
+			   sec->filename, ##__VA_ARGS__)
+
+/*
+ * To avoid breaking uaccess rules, temporarily disable uaccess
+ * before calling printk.
+ */
+#define dbg_sec_uaccess(fmt, ...)					\
+({									\
+	DEFINE_DYNAMIC_DEBUG_METADATA(descriptor, fmt);			\
+	if (DYNAMIC_DEBUG_BRANCH(descriptor)) {				\
+		user_read_access_end();					\
+		__dbg_sec_descriptor(fmt, ##__VA_ARGS__);		\
+		BUG_ON(!user_read_access_begin(				\
+				(void __user *)sec->sframe_start,	\
+				sec->sframe_end - sec->sframe_start));	\
+	}								\
+})
+
 static __always_inline void dbg_print_header(struct sframe_section *sec)
 {
 	unsigned long fdes_end;
@@ -27,6 +47,15 @@ static __always_inline void dbg_print_header(struct sframe_section *sec)
 		sec->ra_off, sec->fp_off);
 }
 
+static __always_inline void dbg_print_fde_uaccess(struct sframe_section *sec,
+						  struct sframe_fde *fde)
+{
+	dbg_sec_uaccess("FDE: start_addr:0x%x func_size:0x%x "
+			"fres_off:0x%x fres_num:%d info:%u rep_size:%u\n",
+			fde->start_addr, fde->func_size,
+			fde->fres_off, fde->fres_num, fde->info, fde->rep_size);
+}
+
 static inline void dbg_init(struct sframe_section *sec)
 {
 	struct mm_struct *mm = current->mm;
@@ -53,8 +82,10 @@ static inline void dbg_free(struct sframe_section *sec)
 
 #define dbg(args...)			no_printk(args)
 #define dbg_sec(args...	)		no_printk(args)
+#define dbg_sec_uaccess(args...)	no_printk(args)
 
 static inline void dbg_print_header(struct sframe_section *sec) {}
+static inline void dbg_print_fde_uaccess(struct sframe_section *sec, struct sframe_fde *fde) {}
 
 static inline void dbg_init(struct sframe_section *sec) {}
 static inline void dbg_free(struct sframe_section *sec) {}
Signature · 1 line
2.48.1
#101 lore raw

Re: [PATCH v4 26/39] unwind_user/sframe: Enable debugging in uaccess regions

Quoted 15 lines · Josh Poimboeuf

Objtool warns about calling pr_debug() from uaccess-enabled regions, and rightfully so. Add a dbg_sec_uaccess() macro which temporarily disables uaccess before doing the dynamic printk, and use that to add debug messages throughout the uaccess-enabled regions.

kernel/unwind/sframe.c +1 -1
@@ -53,12 +53,15 @@ static __always_inline int __read_fde(struct sframe_section *sec,
  			      sizeof(struct sframe_fde), Efault);
 
  	ip = sec->sframe_start + fde->start_addr;
-	if (ip < sec->text_start || ip > sec->text_end)
+	if (ip < sec->text_start || ip > sec->text_end) {

kernel/unwind/sframe.c static __always_inline int __read_fde(struct sframe_section *sec, · line 66

Quoted 10 lines
kernel/unwind/sframe.c +3 -0
+		dbg_sec_uaccess("bad fde num %d\n", fde_num);
  		return -EINVAL;
+	}
 
  	return 0;
 
  Efault:
+	dbg_sec_uaccess("fde %d usercopy failed\n", fde_num);
  	return -EFAULT;
  }

Add a similar debug message for SFRame FDE user copy failures?

kernel/unwind/sframe.c +0 -0

@@ -125,6 +125,7 @@ static __always_inline int __find_fde(struct sframe_section *sec,

        return 0;

Efault:

+       dbg_sec_uaccess("fde usercopy failed\n");
        return -EFAULT;

}

Printing the IP is probably not an option due to security concerns? Printing the the CFA, FP, and RA offsets is too much traffic? To debug issues on s390 I had to add tons of additional debug messages to make sense of what was actually going on.

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#102 lore raw

Re: [PATCH v4 26/39] unwind_user/sframe: Enable debugging in uaccess regions

kernel/unwind/sframe.c static __always_inline int __find_fde(struct sframe_section *sec, · line 130

Quoted 11 lines · Jens Remus

Add a similar debug message for SFRame FDE user copy failures?

kernel/unwind/sframe.c +1 -0
@@ -125,6 +125,7 @@ static __always_inline int __find_fde(struct sframe_section *sec,
        return 0;
 
 Efault:
+       dbg_sec_uaccess("fde usercopy failed\n");
        return -EFAULT;
 }

Indeed.

Quoted 4 lines

Printing the IP is probably not an option due to security concerns? Printing the the CFA, FP, and RA offsets is too much traffic? To debug issues on s390 I had to add tons of additional debug messages to make sense of what was actually going on.

I guess it depends on what you're trying to debug. These messages are intended to help diagnose problems with the section format. It gets unloaded if any of these errors are detected, so it helps to try to communicate why that happened.

So yeah, they're intended to be very low traffic, only used for errors where an .sframe section is getting unloaded (i.e., all the -EFAULTs).

Corrupt CFA/FP/RA offset values are harder to detect, that could also be related to some other issue like stack corruption.

Signature · 1 line
Josh
author maintainer
#103 lore raw

PATCH v4 27/39 unwind_user/sframe: Add .sframe validation option

In reply to Josh Poimboeuf

Add a debug feature to validate all .sframe sections when first loading the file rather than on demand.

---
 arch/Kconfig           | 19 ++++++++++
 kernel/unwind/sframe.c | 81 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 100 insertions(+)
arch/Kconfig +19 -0
index 12a3b73cbe66..b3676605bab6 100644
@@ -453,6 +453,25 @@ config HAVE_UNWIND_USER_SFRAME
 config AS_SFRAME
 	def_bool $(as-instr,.cfi_sections .sframe\n.cfi_startproc\n.cfi_endproc)
 
+config SFRAME_VALIDATION
+	bool "Enable .sframe section debugging"
+	depends on HAVE_UNWIND_USER_SFRAME
+	depends on DYNAMIC_DEBUG
+	help
+	  When adding an .sframe section for a task, validate the entire
+	  section immediately rather than on demand.
+
+	  This is a debug feature which is helpful for rooting out .sframe
+	  section issues.  If the .sframe section is corrupt, it will fail to
+	  load immediately, with more information provided in dynamic printks.
+
+	  This has a significant page cache footprint due to its reading of the
+	  entire .sframe section for every loaded executable and shared
+	  library.  Also, it's done for all processes, even those which don't
+	  get stack traced by the kernel.  Not recommended for general use.
+
+	  If unsure, say N.
+
 config HAVE_PERF_REGS
 	bool
 	help
kernel/unwind/sframe.c +81 -0

93 lines not on this page · raw message

Signature · 1 line
2.48.1
author maintainer
#104 lore raw

PATCH v4 28/39 unwind_user/deferred: Add deferred unwinding interface

In reply to Josh Poimboeuf

Add an interface for scheduling task work to unwind the user space stack before returning to user space. This solves several problems for its callers:

- Ensure the unwind happens in task context even if the caller may be running in NMI or interrupt context.

- Avoid duplicate unwinds, whether called multiple times by the same caller or by different callers.

- Create a "context cookie" which allows trace post-processing to correlate kernel unwinds/traces with the user unwind.

---
 include/linux/entry-common.h          |   2 +
 include/linux/sched.h                 |   5 +
 include/linux/unwind_deferred.h       |  46 +++++++
 include/linux/unwind_deferred_types.h |  10 ++
 kernel/fork.c                         |   4 +
 kernel/unwind/Makefile                |   2 +-
 kernel/unwind/deferred.c              | 178 ++++++++++++++++++++++++++
 7 files changed, 246 insertions(+), 1 deletion(-)
 create mode 100644 include/linux/unwind_deferred.h
 create mode 100644 include/linux/unwind_deferred_types.h
 create mode 100644 kernel/unwind/deferred.c
include/linux/entry-common.h +2 -0
index fc61d0205c97..fb2b27154fee 100644
@@ -12,6 +12,7 @@
 #include <linux/resume_user_mode.h>
 #include <linux/tick.h>
 #include <linux/kmsan.h>
+#include <linux/unwind_deferred.h>
 
 #include <asm/entry-common.h>
 
@@ -111,6 +112,7 @@ static __always_inline void enter_from_user_mode(struct pt_regs *regs)
 
 	CT_WARN_ON(__ct_state() != CT_STATE_USER);
 	user_exit_irqoff();
+	unwind_enter_from_user_mode();
 
 	instrumentation_begin();
 	kmsan_unpoison_entry_regs(regs);
include/linux/sched.h +5 -0
index 64934e0830af..042a95f4f6e6 100644
@@ -46,6 +46,7 @@
 #include <linux/rv.h>
 #include <linux/livepatch_sched.h>
 #include <linux/uidgid_types.h>
+#include <linux/unwind_deferred_types.h>
 #include <asm/kmap_size.h>
 
 /* task_struct member predeclarations (sorted alphabetically): */
@@ -1603,6 +1604,10 @@ struct task_struct {
 	struct user_event_mm		*user_event_mm;
 #endif
 
+#ifdef CONFIG_UNWIND_USER
+	struct unwind_task_info		unwind_info;
+#endif
+
 	/*
 	 * New fields for task_struct should be added above here, so that
 	 * they are included in the randomized portion of task_struct.
include/linux/unwind_deferred.h new file +46 -0

46 lines not on this page · raw message

include/linux/unwind_deferred_types.h new file +10 -0
new file mode 100644
index 000000000000..9749824aea09
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_UNWIND_USER_DEFERRED_TYPES_H
+#define _LINUX_UNWIND_USER_DEFERRED_TYPES_H
+
+struct unwind_task_info {
+	unsigned long		*entries;
+	u64			cookie;
+};
+
+#endif /* _LINUX_UNWIND_USER_DEFERRED_TYPES_H */
kernel/fork.c +4 -0
index 88753f8bbdd3..c9a954af72a1 100644
@@ -106,6 +106,7 @@
 #include <linux/pidfs.h>
 #include <linux/tick.h>
 #include <linux/sframe.h>
+#include <linux/unwind_deferred.h>
 
 #include <asm/pgalloc.h>
 #include <linux/uaccess.h>
@@ -973,6 +974,7 @@ void __put_task_struct(struct task_struct *tsk)
 	WARN_ON(refcount_read(&tsk->usage));
 	WARN_ON(tsk == current);
 
+	unwind_task_free(tsk);
 	sched_ext_free(tsk);
 	io_uring_free(tsk);
 	cgroup_free(tsk);
@@ -2370,6 +2372,8 @@ __latent_entropy struct task_struct *copy_process(
 	p->bpf_ctx = NULL;
 #endif
 
+	unwind_task_init(p);
+
 	/* Perform scheduler related setup. Assign this task to a CPU. */
 	retval = sched_fork(clone_flags, p);
 	if (retval)
kernel/unwind/Makefile +1 -1
index f70380d7a6a6..146038165865 100644
@@ -1,2 +1,2 @@
- obj-$(CONFIG_UNWIND_USER)		+= user.o
+ obj-$(CONFIG_UNWIND_USER)		+= user.o deferred.o
  obj-$(CONFIG_HAVE_UNWIND_USER_SFRAME)	+= sframe.o
kernel/unwind/deferred.c new file +178 -0

178 lines not on this page · raw message

Signature · 1 line
2.48.1
maintainer
#105 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 3 lines · Josh Poimboeuf

+/*
+ * The context cookie is a unique identifier which allows post-processing to
+ * correlate kernel trace(s) with user unwinds. The high 12 bits are the CPU

s/12/16/ ?

Quoted 7 lines

+ * id; the lower 48 bits are a per-CPU entry counter.
+ */

kernel/unwind/deferred.c new file +5 -0
+static u64 ctx_to_cookie(u64 cpu, u64 ctx)
+{
+	BUILD_BUG_ON(NR_CPUS > 65535);
+	return (ctx & ((1UL << 48) - 1)) | (cpu << 48);
+}
maintainer
#106 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 7 lines · Peter Zijlstra
Quoted 3 lines · Josh Poimboeuf

+/*
+ * The context cookie is a unique identifier which allows post-processing to
+ * correlate kernel trace(s) with user unwinds. The high 12 bits are the CPU

s/12/16/ ?

Quoted 7 lines

kernel/unwind/deferred.c line 28

Quoted 7 lines

+ * id; the lower 48 bits are a per-CPU entry counter.
+ */

kernel/unwind/deferred.c new file +5 -0
+static u64 ctx_to_cookie(u64 cpu, u64 ctx)
+{
+	BUILD_BUG_ON(NR_CPUS > 65535);
+	return (ctx & ((1UL << 48) - 1)) | (cpu << 48);
+}

Also, I have to note that 0 is a valid return value here, which will give a ton of fun.

author maintainer
#107 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 8 lines · Peter Zijlstra
Quoted 7 lines · Peter Zijlstra
Quoted 3 lines · Josh Poimboeuf

+/*
+ * The context cookie is a unique identifier which allows post-processing to
+ * correlate kernel trace(s) with user unwinds. The high 12 bits are the CPU

s/12/16/ ?

Quoted 10 lines
Quoted 7 lines

kernel/unwind/deferred.c line 28

Quoted 7 lines

+ * id; the lower 48 bits are a per-CPU entry counter.
+ */

kernel/unwind/deferred.c new file +5 -0
+static u64 ctx_to_cookie(u64 cpu, u64 ctx)
+{
+	BUILD_BUG_ON(NR_CPUS > 65535);
+	return (ctx & ((1UL << 48) - 1)) | (cpu << 48);
+}

Also, I have to note that 0 is a valid return value here, which will give a ton of fun.

The ctx_ctr is always incremented before calling this, so 0 isn't a valid cookie.

Signature · 1 line
Josh
maintainer
#108 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 20 lines · Josh Poimboeuf
Quoted 8 lines · Peter Zijlstra
Quoted 7 lines · Peter Zijlstra
Quoted 3 lines · Josh Poimboeuf

+/*
+ * The context cookie is a unique identifier which allows post-processing to
+ * correlate kernel trace(s) with user unwinds. The high 12 bits are the CPU

s/12/16/ ?

Quoted 10 lines
Quoted 7 lines

kernel/unwind/deferred.c line 28

Quoted 7 lines

+ * id; the lower 48 bits are a per-CPU entry counter.
+ */

kernel/unwind/deferred.c new file +5 -0
+static u64 ctx_to_cookie(u64 cpu, u64 ctx)
+{
+	BUILD_BUG_ON(NR_CPUS > 65535);
+	return (ctx & ((1UL << 48) - 1)) | (cpu << 48);
+}

Also, I have to note that 0 is a valid return value here, which will give a ton of fun.

Quoted 2 lines

The ctx_ctr is always incremented before calling this, so 0 isn't a valid cookie.

Right, so that's the problem. You're considering 0 an invalid cookie, but ctx_to_cookie(0, 1<<48) will be a 0 cookie.

That thing *will* wrap.

author maintainer
#109 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 9 lines · Peter Zijlstra
Quoted 3 lines · Josh Poimboeuf

On Wed, Jan 22, 2025 at 03:16:16PM +0100, Peter Zijlstra wrote:
The ctx_ctr is always incremented before calling this, so 0 isn't a valid cookie.

Right, so that's the problem. You're considering 0 an invalid cookie, but ctx_to_cookie(0, 1<<48) will be a 0 cookie.

That thing *will* wrap.

Well, yes, after N years of sustained very high syscall activity on CPU 0, with stack tracing enabled, in which multiple tracer unwind requests happen to occur in the same entry context where ctx_ctr wrapped, one of the tracers might get an invalid cookie.

I can double-increment the counter when it's (1UL << 48) - 1). Or use some other bit for "cookie valid".

Signature · 1 line
Josh
maintainer
#110 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 11 lines · Josh Poimboeuf
Quoted 9 lines · Peter Zijlstra
Quoted 3 lines · Josh Poimboeuf

On Wed, Jan 22, 2025 at 03:16:16PM +0100, Peter Zijlstra wrote:
The ctx_ctr is always incremented before calling this, so 0 isn't a valid cookie.

Right, so that's the problem. You're considering 0 an invalid cookie, but ctx_to_cookie(0, 1<<48) will be a 0 cookie.

That thing *will* wrap.

Quoted 7 lines

Well, yes, after N years of sustained very high syscall activity on CPU 0, with stack tracing enabled, in which multiple tracer unwind requests happen to occur in the same entry context where ctx_ctr wrapped, one of the tracers might get an invalid cookie.

I can double-increment the counter when it's (1UL << 48) - 1). Or use some other bit for "cookie valid".

Right, steal one bit from counter and make it always 1. 47 bit wrap around should be fine.

author maintainer
#111 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

In reply to Peter Zijlstra
Quoted 6 lines · Peter Zijlstra
Quoted 3 lines · Josh Poimboeuf

+/*
+ * The context cookie is a unique identifier which allows post-processing to
+ * correlate kernel trace(s) with user unwinds. The high 12 bits are the CPU

s/12/16/ ?

Oops. Code is right, comment is wrong.

Quoted 8 lines
Quoted 8 lines

+ * id; the lower 48 bits are a per-CPU entry counter.
+ */

kernel/unwind/deferred.c new file +5 -0
+static u64 ctx_to_cookie(u64 cpu, u64 ctx)
+{
+	BUILD_BUG_ON(NR_CPUS > 65535);
+	return (ctx & ((1UL << 48) - 1)) | (cpu << 48);
+}
Signature · 1 line
Josh
maintainer
#112 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

In reply to Josh Poimboeuf

kernel/unwind/deferred.c line 17

Quoted 2 lines · Josh Poimboeuf
kernel/unwind/deferred.c new file +2 -0
+/* entry-from-user counter */
+static DEFINE_PER_CPU(u64, unwind_ctx_ctr);

AFAICT from the below, this thing does *not* count entry-from-user. It might count a subset, but I need to stare longer.

Quoted 39 lines
kernel/unwind/deferred.c new file +34 -0
+/*
+ * Read the task context cookie, first initializing it if this is the first
+ * call to get_cookie() since the most recent entry from user.
+ */
+static u64 get_cookie(struct unwind_task_info *info)
+{
+	u64 ctx_ctr;
+	u64 cookie;
+	u64 cpu;
+
+	guard(irqsave)();
+
+	cookie = info->cookie;
+	if (cookie)
+		return cookie;
+
+
+	cpu = raw_smp_processor_id();
+	ctx_ctr = __this_cpu_inc_return(unwind_ctx_ctr);
+	info->cookie = ctx_to_cookie(cpu, ctx_ctr);
+
+	return cookie;
+
+}
+
+static void unwind_deferred_task_work(struct callback_head *head)
+{
 
+	cookie = get_cookie(info);
 
+}
+
 
+int unwind_deferred_request(struct unwind_work *work, u64 *cookie)
+{
 
+	*cookie = get_cookie(info);
 
+}
author maintainer
#113 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 7 lines · Peter Zijlstra

kernel/unwind/deferred.c line 17

Quoted 2 lines · Josh Poimboeuf
kernel/unwind/deferred.c new file +2 -0
+/* entry-from-user counter */
+static DEFINE_PER_CPU(u64, unwind_ctx_ctr);

AFAICT from the below, this thing does *not* count entry-from-user. It might count a subset, but I need to stare longer.

Right, it's a subset. Something like so:

/*
* This is a unique percpu identifier for a given task entry context.
* Conceptually, it's incremented every time the CPU enters the kernel from
* user space, so that each "entry context" on the CPU gets a unique ID. In
* reality, as an optimization, it's only incremented on demand for the first
* deferred unwind request after a given entry-from-user.
*
* It's combined with the CPU id to make a systemwide-unique "context cookie". */
static DEFINE_PER_CPU(u64, unwind_ctx_ctr);

Signature · 1 line
Josh
#114 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

In reply to Josh Poimboeuf
Quoted 83 lines · Josh Poimboeuf

Add an interface for scheduling task work to unwind the user space stack before returning to user space. This solves several problems for its callers:

- Ensure the unwind happens in task context even if the caller may be running in NMI or interrupt context.

- Avoid duplicate unwinds, whether called multiple times by the same caller or by different callers.

- Create a "context cookie" which allows trace post-processing to correlate kernel unwinds/traces with the user unwind.

---
  include/linux/entry-common.h          |   2 +
  include/linux/sched.h                 |   5 +
  include/linux/unwind_deferred.h       |  46 +++++++
  include/linux/unwind_deferred_types.h |  10 ++
  kernel/fork.c                         |   4 +
  kernel/unwind/Makefile                |   2 +-
  kernel/unwind/deferred.c              | 178 ++++++++++++++++++++++++++
  7 files changed, 246 insertions(+), 1 deletion(-)

create mode 100644 include/linux/unwind_deferred.h create mode 100644 include/linux/unwind_deferred_types.h create mode 100644 kernel/unwind/deferred.c

include/linux/entry-common.h +2 -0
index fc61d0205c97..fb2b27154fee 100644
@@ -12,6 +12,7 @@
  #include <linux/resume_user_mode.h>
  #include <linux/tick.h>
  #include <linux/kmsan.h>
+#include <linux/unwind_deferred.h>
 
  #include <asm/entry-common.h>
 
@@ -111,6 +112,7 @@ static __always_inline void enter_from_user_mode(struct pt_regs *regs)
 
  	CT_WARN_ON(__ct_state() != CT_STATE_USER);
  	user_exit_irqoff();
+	unwind_enter_from_user_mode();
 
  	instrumentation_begin();
  	kmsan_unpoison_entry_regs(regs);
include/linux/sched.h +5 -0
index 64934e0830af..042a95f4f6e6 100644
@@ -46,6 +46,7 @@
  #include <linux/rv.h>
  #include <linux/livepatch_sched.h>
  #include <linux/uidgid_types.h>
+#include <linux/unwind_deferred_types.h>
  #include <asm/kmap_size.h>
 
  /* task_struct member predeclarations (sorted alphabetically): */
@@ -1603,6 +1604,10 @@ struct task_struct {
  	struct user_event_mm		*user_event_mm;
  #endif
 
+#ifdef CONFIG_UNWIND_USER
+	struct unwind_task_info		unwind_info;
+#endif
+
  	/*
  	 * New fields for task_struct should be added above here, so that
  	 * they are included in the randomized portion of task_struct.
include/linux/unwind_deferred.h new file +7 -0
new file mode 100644
index 000000000000..741f409f0d1f
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_UNWIND_USER_DEFERRED_H
+#define _LINUX_UNWIND_USER_DEFERRED_H
+
+#include <linux/task_work.h>
+#include <linux/unwind_user.h>
+#include <linux/unwind_deferred_types.h>

include/linux/unwind_deferred.h line 17

Quoted 10 lines
include/linux/unwind_deferred.h new file +10 -0
+
+struct unwind_work;
+
+typedef void (*unwind_callback_t)(struct unwind_work *work, struct unwind_stacktrace *trace, u64 cookie);
+
+struct unwind_work {
+	struct callback_head		work;
+	unwind_callback_t		func;
+	int				pending;
+};

This is a lot of information to keep around per instance.

I'm not sure it would be OK to have a single unwind_work per perf-event for perf. I suspect it may need to be per perf-event X per-task if a perf-event can be associated to more than a single task (not sure ?).

For LTTng, we'd have to consider something similar because of multi-session support. Either we'd have one unwind_work per-session X per-task, or we'd need to multiplex this internally within LTTng-modules. None of this is ideal in terms of memory footprint.

We should look at what part of this information can be made static/global and what part is task-local, so we minimize the amount of redundant data per-task (memory footprint).

AFAIU, most of that unwind_work information is global:

- work,
- func,

And could be registered dynamically by the tracer when it enables tracing with an interest on stack walking.

At registration, we can allocate a descriptor ID (with a limited bounded max number, configurable). This would associate a work+func to a given ID, and keep track of this in a global table (indexed by ID).

I suspect that the only thing we really want to keep track of per-task is the pending bit, and what is the ID of the unwind_work associated. This could be kept, per-task, in either:

- a bitmap of pending bits, indexed by ID, or
- an array of pending IDs.

Unregistration of unwind_work could iterate on all tasks and clear the pending bit or ID associated with the unregistered work, to make sure we don't trigger unrelated work after a re-use.

Quoted 104 lines
include/linux/unwind_deferred.h new file +29 -0
+
+#ifdef CONFIG_UNWIND_USER
+
+void unwind_task_init(struct task_struct *task);
+void unwind_task_free(struct task_struct *task);
+
+void unwind_deferred_init(struct unwind_work *work, unwind_callback_t func);
+int unwind_deferred_request(struct unwind_work *work, u64 *cookie);
+bool unwind_deferred_cancel(struct task_struct *task, struct unwind_work *work);
+
+static __always_inline void unwind_enter_from_user_mode(void)
+{
+	current->unwind_info.cookie = 0;
+}
+
+#else /* !CONFIG_UNWIND_USER */
+
+static inline void unwind_task_init(struct task_struct *task) {}
+static inline void unwind_task_free(struct task_struct *task) {}
+
+static inline void unwind_deferred_init(struct unwind_work *work, unwind_callback_t func) {}
+static inline int unwind_deferred_request(struct task_struct *task, struct unwind_work *work, u64 *cookie) { return -ENOSYS; }
+static inline bool unwind_deferred_cancel(struct task_struct *task, struct unwind_work *work) { return false; }
+
+static inline void unwind_enter_from_user_mode(void) {}
+
+#endif /* !CONFIG_UNWIND_USER */
+
+#endif /* _LINUX_UNWIND_USER_DEFERRED_H */
include/linux/unwind_deferred_types.h new file +10 -0
new file mode 100644
index 000000000000..9749824aea09
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_UNWIND_USER_DEFERRED_TYPES_H
+#define _LINUX_UNWIND_USER_DEFERRED_TYPES_H
+
+struct unwind_task_info {
+	unsigned long		*entries;
+	u64			cookie;
+};
+
+#endif /* _LINUX_UNWIND_USER_DEFERRED_TYPES_H */
kernel/fork.c +4 -0
index 88753f8bbdd3..c9a954af72a1 100644
@@ -106,6 +106,7 @@
  #include <linux/pidfs.h>
  #include <linux/tick.h>
  #include <linux/sframe.h>
+#include <linux/unwind_deferred.h>
 
  #include <asm/pgalloc.h>
  #include <linux/uaccess.h>
@@ -973,6 +974,7 @@ void __put_task_struct(struct task_struct *tsk)
  	WARN_ON(refcount_read(&tsk->usage));
  	WARN_ON(tsk == current);
 
+	unwind_task_free(tsk);
  	sched_ext_free(tsk);
  	io_uring_free(tsk);
  	cgroup_free(tsk);
@@ -2370,6 +2372,8 @@ __latent_entropy struct task_struct *copy_process(
  	p->bpf_ctx = NULL;
  #endif
 
+	unwind_task_init(p);
+
  	/* Perform scheduler related setup. Assign this task to a CPU. */
  	retval = sched_fork(clone_flags, p);
  	if (retval)
kernel/unwind/Makefile +1 -1
index f70380d7a6a6..146038165865 100644
@@ -1,2 +1,2 @@
- obj-$(CONFIG_UNWIND_USER)		+= user.o
+ obj-$(CONFIG_UNWIND_USER)		+= user.o deferred.o
   obj-$(CONFIG_HAVE_UNWIND_USER_SFRAME)	+= sframe.o
kernel/unwind/deferred.c new file +16 -0
new file mode 100644
index 000000000000..f0dbe4069247
@@ -0,0 +1,178 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+* Deferred user space unwinding
+*/
+#include <linux/kernel.h>
+#include <linux/sched.h>
+#include <linux/sched/task_stack.h>
+#include <linux/sframe.h>
+#include <linux/slab.h>
+#include <linux/task_work.h>
+#include <linux/mm.h>
+#include <linux/unwind_deferred.h>
+
+#define UNWIND_MAX_ENTRIES 512
+
+/* entry-from-user counter */

kernel/unwind/deferred.c line 26

Quoted 10 lines
kernel/unwind/deferred.c new file +10 -0
+static DEFINE_PER_CPU(u64, unwind_ctx_ctr);
+
+/*
+ * The context cookie is a unique identifier which allows post-processing to
+ * correlate kernel trace(s) with user unwinds.  The high 12 bits are the CPU
+ * id; the lower 48 bits are a per-CPU entry counter.
+ */
+static u64 ctx_to_cookie(u64 cpu, u64 ctx)
+{
+	BUILD_BUG_ON(NR_CPUS > 65535);

2^12 = 4k, not 64k. Perhaps you mean to reserve 16 bits for cpu numbers ?

kernel/unwind/deferred.c line 27

Quoted 1 line
kernel/unwind/deferred.c new file +1 -0
+	return (ctx & ((1UL << 48) - 1)) | (cpu << 48);

Perhaps use ilog2(NR_CPUS) instead for the number of bits to use rather than hard code 12 ?

Quoted 59 lines
kernel/unwind/deferred.c new file +59 -0
+}
+
+/*
+ * Read the task context cookie, first initializing it if this is the first
+ * call to get_cookie() since the most recent entry from user.
+ */
+static u64 get_cookie(struct unwind_task_info *info)
+{
+	u64 ctx_ctr;
+	u64 cookie;
+	u64 cpu;
+
+	guard(irqsave)();
+
+	cookie = info->cookie;
+	if (cookie)
+		return cookie;
+
+
+	cpu = raw_smp_processor_id();
+	ctx_ctr = __this_cpu_inc_return(unwind_ctx_ctr);
+	info->cookie = ctx_to_cookie(cpu, ctx_ctr);
+
+	return cookie;
+
+}
+
+static void unwind_deferred_task_work(struct callback_head *head)
+{
+	struct unwind_work *work = container_of(head, struct unwind_work, work);
+	struct unwind_task_info *info = &current->unwind_info;
+	struct unwind_stacktrace trace;
+	u64 cookie;
+
+	if (WARN_ON_ONCE(!work->pending))
+		return;
+
+	/*
+	 * From here on out, the callback must always be called, even if it's
+	 * just an empty trace.
+	 */
+
+	cookie = get_cookie(info);
+
+	/* Check for task exit path. */
+	if (!current->mm)
+		goto do_callback;
+
+	if (!info->entries) {
+		info->entries = kmalloc(UNWIND_MAX_ENTRIES * sizeof(long),
+					GFP_KERNEL);
+		if (!info->entries)
+			goto do_callback;
+	}
+
+	trace.entries = info->entries;
+	trace.nr = 0;
+	unwind_user(&trace, UNWIND_MAX_ENTRIES);
+

kernel/unwind/deferred.c line 96

Quoted 10 lines
kernel/unwind/deferred.c new file +10 -0
+do_callback:
+	work->func(work, &trace, cookie);
+	work->pending = 0;
+}
+
+/*
+ * Schedule a user space unwind to be done in task work before exiting the
+ * kernel.
+ *
+ * The returned cookie output is a unique identifer for the current task entry

identifier

Thanks,

Mathieu

Quoted 82 lines
kernel/unwind/deferred.c new file +82 -0
+ * context.  Its value will also be passed to the callback function.  It can be
+ * used to stitch kernel and user stack traces together in post-processing.
+ *
+ * It's valid to call this function multiple times for the same @work within
+ * the same task entry context.  Each call will return the same cookie.  If the
+ * callback is already pending, an error will be returned along with the
+ * cookie.  If the callback is not pending because it has already been
+ * previously called for the same entry context, it will be called again with
+ * the same stack trace and cookie.
+ *
+ * Thus are three possible return scenarios:
+ *
+ *   * return != 0, *cookie == 0: the operation failed, no pending callback.
+ *
+ *   * return != 0, *cookie != 0: the callback is already pending. The cookie
+ *     can still be used to correlate with the pending callback.
+ *
+ *   * return == 0, *cookie != 0: the callback queued successfully.  The
+ *     callback is guaranteed to be called with the given cookie.
+ */
+int unwind_deferred_request(struct unwind_work *work, u64 *cookie)
+{
+	struct unwind_task_info *info = &current->unwind_info;
+	int ret;
+
+	*cookie = 0;
+
+	if (WARN_ON_ONCE(in_nmi()))
+		return -EINVAL;
+
+	if (!current->mm || !user_mode(task_pt_regs(current)))
+		return -EINVAL;
+
+	guard(irqsave)();
+
+	*cookie = get_cookie(info);
+
+	/* callback already pending? */
+	if (work->pending)
+		return -EEXIST;
+
+	ret = task_work_add(current, &work->work, TWA_RESUME);
+	if (WARN_ON_ONCE(ret))
+		return ret;
+
+	work->pending = 1;
+
+	return 0;
+}
+
+bool unwind_deferred_cancel(struct task_struct *task, struct unwind_work *work)
+{
+	bool ret;
+
+	ret = task_work_cancel(task, &work->work);
+	if (ret)
+		work->pending = 0;
+
+	return ret;
+}
+
+void unwind_deferred_init(struct unwind_work *work, unwind_callback_t func)
+{
+	memset(work, 0, sizeof(*work));
+
+	init_task_work(&work->work, unwind_deferred_task_work);
+	work->func = func;
+}
+
+void unwind_task_init(struct task_struct *task)
+{
+	struct unwind_task_info *info = &task->unwind_info;
+
+	memset(info, 0, sizeof(*info));
+}
+
+void unwind_task_free(struct task_struct *task)
+{
+	struct unwind_task_info *info = &task->unwind_info;
+
+	kfree(info->entries);
+}
Signature · 3 lines
Mathieu Desnoyers
EfficiOS Inc.
https://www.efficios.com
author maintainer
#115 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 11 lines · Mathieu Desnoyers

include/linux/unwind_deferred.h line 17

Quoted 5 lines
include/linux/unwind_deferred.h new file +5 -0
+struct unwind_work {
+	struct callback_head		work;
+	unwind_callback_t		func;
+	int				pending;
+};

This is a lot of information to keep around per instance.

I'm not sure it would be OK to have a single unwind_work per perf-event for perf. I suspect it may need to be per perf-event X per-task if a perf-event can be associated to more than a single task (not sure ?).

For "perf record -g <command>", it seems to be one event per task. Incidentally this is the mode where I did my perf testing :-/

But looking at it now, a global "perf record -g" appears to use one event per CPU. So if a task requests an unwind and then schedules out before returning to user space, any subsequent tasks trying to unwind on that CPU would be blocked until the original task returned to user. So yeah, that's definitely a problem.

Actually a per-CPU unwind_work descriptor could conceivably work if we able to unwind at schedule() time.

But Steve pointed out that wouldn't work so well if the task isn't in RUNNING state.

However... would it be a horrible idea for 'next' to unwind 'prev' after the context switch???

Quoted 21 lines

For LTTng, we'd have to consider something similar because of multi-session support. Either we'd have one unwind_work per-session X per-task, or we'd need to multiplex this internally within LTTng-modules. None of this is ideal in terms of memory footprint.

We should look at what part of this information can be made static/global and what part is task-local, so we minimize the amount of redundant data per-task (memory footprint).

AFAIU, most of that unwind_work information is global:

- work,
- func,

And could be registered dynamically by the tracer when it enables tracing with an interest on stack walking.

At registration, we can allocate a descriptor ID (with a limited bounded max number, configurable). This would associate a work+func to a given ID, and keep track of this in a global table (indexed by ID).

Quoted 6 lines

I suspect that the only thing we really want to keep track of per-task is the pending bit, and what is the ID of the unwind_work associated. This could be kept, per-task, in either:

- a bitmap of pending bits, indexed by ID, or
- an array of pending IDs.

That's basically what I was doing before. The per-task state also had:

- 'struct callback_head work' for doing the task work. A single work function was used to multiplex the callbacks, as opposed to the current patches where each descriptor gets its own separate task_work.

- 'void *privs[UNWIND_MAX_CALLBACKS]' opaque data pointers. Maybe some callbacks don't need that, but perf needed it for the 'event' pointer. For 32 max callbacks that's 256 bytes per task.

- 'u64 last_cookies[UNWIND_MAX_CALLBACKS]' to prevent a callback from getting called twice. But actually that may have been overkill, it should be fine to call the callback again with the cached stack trace. The tracer could instead have its own policy for how to handle dupes.

- 'unsigned int work_pending' to designate whether the task_work is pending. Also probably not necessary, the pending bits could serve the same purpose.

So it had more concurrency to deal with, to handle the extra per-task state.

It also had a global array of callbacks, which used a mutex and SRCU to coordinate between the register/unregister and the task work.

Another major issue was that it wasn't NMI-safe due to all the shared state. So a tracer in NMI would have to schedule an IRQ to call unwind_deferred_request(). Not only is that a pain for the tracers, it's problematic in other ways:

- If the NMI occurred in schedule() with IRQs disabled, the IRQ would actually interrupt the 'next' task. So the caller would have to stash a 'task' pointer for the IRQ handler to read and pass to unwind_deferred_request(). (similar to the task_work bug I found)

- Thus the deferred unwind interface would need to handle requests from non-current, introducing a new set of concurrency issues.

- Also, while a tracer in NMI can unwind the kernel stack and send that to a ring buffer immediately, it can't store the cookie along with it, so there lie more tracer headaches.

Once I changed the interface to get rid of the global nastiness, all those problems went away.

Of course that now introduces the new problem that each tracer (or tracing event) needs some kind of per-task state. But otherwise this new interface really simplifies things a *lot*.

Anyway, I don't have a good answer at the moment. Will marinate on it.

Maybe we could do something like allocate the unwind_work (or some equivalent) on demand at the time of unwind request using GFP_NOWAIT or GFP_ATOMIC or some such, then free it during the task work?

Quoted 3 lines

Unregistration of unwind_work could iterate on all tasks and clear the pending bit or ID associated with the unregistered work, to make sure we don't trigger unrelated work after a re-use.

What the old unregister code did was to remove it from the global callbacks array (with the careful use of mutex+SRCU to coordinate with the task work). Then synchronize_srcu() before returning.

Quoted 11 lines

kernel/unwind/deferred.c line 26

Quoted 8 lines
kernel/unwind/deferred.c new file +8 -0
+/*
+ * The context cookie is a unique identifier which allows post-processing to
+ * correlate kernel trace(s) with user unwinds.  The high 12 bits are the CPU
+ * id; the lower 48 bits are a per-CPU entry counter.
+ */
+static u64 ctx_to_cookie(u64 cpu, u64 ctx)
+{
+	BUILD_BUG_ON(NR_CPUS > 65535);

2^12 = 4k, not 64k. Perhaps you mean to reserve 16 bits for cpu numbers ?

Yeah, here the code is right but the comment is wrong. It actually does use 16 bits.

Quoted 4 lines

kernel/unwind/deferred.c line 27

Quoted 1 line
kernel/unwind/deferred.c new file +1 -0
+	return (ctx & ((1UL << 48) - 1)) | (cpu << 48);

Perhaps use ilog2(NR_CPUS) instead for the number of bits to use rather than hard code 12 ?

I'm thinking I'd rather keep it simple by hard-coding the # of bits, so as to avoid any surprises caused by edge cases.

Signature · 1 line
Josh
maintainer
#116 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 2 lines · Josh Poimboeuf

However... would it be a horrible idea for 'next' to unwind 'prev' after the context switch???

The idea isn't terrible, but it will be all sorta of tricky.

The big immediate problem is that the CPU doing the context switch looses control over prev at:

__schedule()

    context_switch()
      finish_task_switch()
        finish_task()
	  smp_store_release(&prev->on_cpu, 0);

And this is before we drop rq->lock.

The instruction after that store another CPU is free to claim the task and run with it. Notably, another CPU might already be spin waiting on that state, trying to wake the task back up.

By the time we get to a schedulable context, @prev is completely out of bounds.

author maintainer
#117 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 16 lines · Peter Zijlstra
Quoted 2 lines · Josh Poimboeuf

However... would it be a horrible idea for 'next' to unwind 'prev' after the context switch???

The idea isn't terrible, but it will be all sorta of tricky.

The big immediate problem is that the CPU doing the context switch looses control over prev at:

__schedule()

    context_switch()
      finish_task_switch()
        finish_task()
	  smp_store_release(&prev->on_cpu, 0);
Quoted 8 lines

And this is before we drop rq->lock.

The instruction after that store another CPU is free to claim the task and run with it. Notably, another CPU might already be spin waiting on that state, trying to wake the task back up.

By the time we get to a schedulable context, @prev is completely out of bounds.

Could unwind_deferred_request() call migrate_disable() or so?

How bad would it be to set some bit in @prev to prevent it from getting rescheduled until the unwind from @next has been done? Unfortunately two tasks would be blocked on the unwind instead of one.

BTW, this might be useful for another reason. In Steve's sframe meeting yesterday there was some talk of BPF needing to unwind from sched-switch, without having to wait indefinitely for @prev to get rescheduled and return to user.

Signature · 1 line
Josh
maintainer
#118 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 19 lines · Josh Poimboeuf
Quoted 18 lines · Peter Zijlstra
Quoted 2 lines · Josh Poimboeuf

However... would it be a horrible idea for 'next' to unwind 'prev' after the context switch???

The idea isn't terrible, but it will be all sorta of tricky.

The big immediate problem is that the CPU doing the context switch looses control over prev at:

__schedule()

    context_switch()
      finish_task_switch()
        finish_task()
	  smp_store_release(&prev->on_cpu, 0);

And this is before we drop rq->lock.

Quoted 8 lines
Quoted 6 lines

The instruction after that store another CPU is free to claim the task and run with it. Notably, another CPU might already be spin waiting on that state, trying to wake the task back up.

By the time we get to a schedulable context, @prev is completely out of bounds.

Could unwind_deferred_request() call migrate_disable() or so?

That's pretty vile... and might cause performance issues. You realy don't want things to magically start behaving differently just because you're tracing.

Quoted 3 lines

How bad would it be to set some bit in @prev to prevent it from getting rescheduled until the unwind from @next has been done? Unfortunately two tasks would be blocked on the unwind instead of one.

Yeah, not going to happen. Those paths are complicated enough as is.

Quoted 4 lines

BTW, this might be useful for another reason. In Steve's sframe meeting yesterday there was some talk of BPF needing to unwind from sched-switch, without having to wait indefinitely for @prev to get rescheduled and return to user.

-EPONIES, you cannot take faults from the middle of schedule(). They can always use the best effort FP unwind we have today.

maintainer
#119 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 2 lines · On Thu, 23 Jan 2025 23:13:26 +0100 Peter Zijlstra <[email protected]> wrote:

-EPONIES, you cannot take faults from the middle of schedule(). They can always use the best effort FP unwind we have today.

Agreed.

Now the only thing I could think of is a flag gets set where the task comes out of the scheduler and then does the stack trace. It doesn't need to do the stack trace before it schedules. As it did just schedule, where ever it scheduled must have been in a schedulable context.

That is, kind of like the task_work flag for entering user space and exiting the kernel, could we have a sched_work flag to run after after being scheduled back (exiting schedule()). Since the task has been picked to run, it will not cause latency for other tasks. The work will be done in its context. This is no different to the tasks accounting than if it does this going back to user space. Heck, it would only need to do this once if it didn't go back to user space, as the user space stack would be the same. That is, if it gets scheduled multiple times, this would only happen on the first instance until it leaves the kernel.

	[ trigger stack trace - set sched_work ]

	schedule() {
		context_switch() -> CPU runs some other task
				 <- gets scheduled back onto the CPU
		[..]
		/* preemption enabled ... */
		if (sched_work) {
			do stack trace() // can schedule here but
					 // calls a schedule function that does not
					 // do sched_work to prevent recursion
		}
	}

Could something like this work?

-- Steve

author maintainer
#120 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 38 lines · Steven Rostedt
Quoted 2 lines · On Thu, 23 Jan 2025 23:13:26 +0100 Peter Zijlstra <[email protected]> wrote:

-EPONIES, you cannot take faults from the middle of schedule(). They can always use the best effort FP unwind we have today.

Agreed.

Now the only thing I could think of is a flag gets set where the task comes out of the scheduler and then does the stack trace. It doesn't need to do the stack trace before it schedules. As it did just schedule, where ever it scheduled must have been in a schedulable context.

That is, kind of like the task_work flag for entering user space and exiting the kernel, could we have a sched_work flag to run after after being scheduled back (exiting schedule()). Since the task has been picked to run, it will not cause latency for other tasks. The work will be done in its context. This is no different to the tasks accounting than if it does this going back to user space. Heck, it would only need to do this once if it didn't go back to user space, as the user space stack would be the same. That is, if it gets scheduled multiple times, this would only happen on the first instance until it leaves the kernel.

	[ trigger stack trace - set sched_work ]

	schedule() {
		context_switch() -> CPU runs some other task
				 <- gets scheduled back onto the CPU
		[..]
		/* preemption enabled ... */
		if (sched_work) {
			do stack trace() // can schedule here but
					 // calls a schedule function that does not
					 // do sched_work to prevent recursion
		}
	}
Quoted 1 line

Could something like this work?

Yeah, this is basically a more fleshed out version of what I was trying to propose.

One additional wrinkle is that if @prev wakes up on another CPU while @next is unwinding it, the unwind goes haywire. So that would maybe need to be prevented.

Signature · 1 line
Josh
author maintainer
#121 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 31 lines · Josh Poimboeuf
Quoted 30 lines · Steven Rostedt

Now the only thing I could think of is a flag gets set where the task comes out of the scheduler and then does the stack trace. It doesn't need to do the stack trace before it schedules. As it did just schedule, where ever it scheduled must have been in a schedulable context.

That is, kind of like the task_work flag for entering user space and exiting the kernel, could we have a sched_work flag to run after after being scheduled back (exiting schedule()). Since the task has been picked to run, it will not cause latency for other tasks. The work will be done in its context. This is no different to the tasks accounting than if it does this going back to user space. Heck, it would only need to do this once if it didn't go back to user space, as the user space stack would be the same. That is, if it gets scheduled multiple times, this would only happen on the first instance until it leaves the kernel.

	[ trigger stack trace - set sched_work ]

	schedule() {
		context_switch() -> CPU runs some other task
				 <- gets scheduled back onto the CPU
		[..]
		/* preemption enabled ... */
		if (sched_work) {
			do stack trace() // can schedule here but
					 // calls a schedule function that does not
					 // do sched_work to prevent recursion
		}
	}
Quoted 8 lines
Quoted 1 line

Could something like this work?

Yeah, this is basically a more fleshed out version of what I was trying to propose.

One additional wrinkle is that if @prev wakes up on another CPU while @next is unwinding it, the unwind goes haywire. So that would maybe need to be prevented.

Hm, reading this again I'm wondering if you're actually proposing that the unwind happens on @prev after it gets rescheduled sometime in the future? Does that actually solve the issue? What if doesn't get rescheduled within a reasonable amount of time?

Signature · 1 line
Josh
maintainer
#122 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 4 lines · On Fri, 24 Jan 2025 14:50:11 -0800 Josh Poimboeuf <[email protected]> wrote:

Hm, reading this again I'm wondering if you're actually proposing that the unwind happens on @prev after it gets rescheduled sometime in the future? Does that actually solve the issue? What if doesn't get rescheduled within a reasonable amount of time?

Correct, it would be prev that would be doing the unwinding and not next. But when prev is scheduled back onto the CPU. That way it's only blocking itself.

The use case that people were doing this with was measuring the time a task is off the CPU. It can't get that time until the task schedules back anyway. What the complaint was about was that it could be a very long system call, with lots of sleeps and they couldn't do the processing.

I can go back and ask, but I'm pretty sure doing the unwind when a task comes back to the CPU would be sufficient.

-- Steve

maintainer
#123 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 12 lines · On Fri, 24 Jan 2025 18:57:44 -0500 Steven Rostedt <[email protected]> wrote:
Quoted 4 lines · On Fri, 24 Jan 2025 14:50:11 -0800 Josh Poimboeuf <[email protected]> wrote:

Hm, reading this again I'm wondering if you're actually proposing that the unwind happens on @prev after it gets rescheduled sometime in the future? Does that actually solve the issue? What if doesn't get rescheduled within a reasonable amount of time?

Correct, it would be prev that would be doing the unwinding and not next. But when prev is scheduled back onto the CPU. That way it's only blocking itself.

Quoted 8 lines

The use case that people were doing this with was measuring the time a task is off the CPU. It can't get that time until the task schedules back anyway. What the complaint was about was that it could be a very long system call, with lots of sleeps and they couldn't do the processing.

I can go back and ask, but I'm pretty sure doing the unwind when a task comes back to the CPU would be sufficient.

Coming back from this. It would be fine if we could do the back trace when we come back from the scheduler, so it should not be an issue if the task even has to schedule again to fault in the sframe information.

I was also wondering if the unwinder doesn't keep track of who requested the back trace, just that someone did. Then it would just take a single flag in the task struct to do the back trace. Return the "cookie" to the tracer that requested the back trace, and when you do the back trace, just call all callbacks with that cookie. Let the tracer decided if it wants to record the back trace or ignore it based on the cookie.

That is, the tracers would need to keep track of the cookies that it cares about, as if there's other tracers asking for stack traces on tasks that this tracer doesn't care about it needs to handle being called when it doesn't care about the stack trace. That said, if you want to trace all tasks, you can just ignore the cookies and record the traces.

-- Steve

author maintainer
#124 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 3 lines · Steven Rostedt

Coming back from this. It would be fine if we could do the back trace when we come back from the scheduler, so it should not be an issue if the task even has to schedule again to fault in the sframe information.

So there would be two callback hook points:

- schedule() after enabling preemption

- task work

and first one wins?

Quoted 7 lines

I was also wondering if the unwinder doesn't keep track of who requested the back trace, just that someone did. Then it would just take a single flag in the task struct to do the back trace. Return the "cookie" to the tracer that requested the back trace, and when you do the back trace, just call all callbacks with that cookie. Let the tracer decided if it wants to record the back trace or ignore it based on the cookie.

Quoted 5 lines

That is, the tracers would need to keep track of the cookies that it cares about, as if there's other tracers asking for stack traces on tasks that this tracer doesn't care about it needs to handle being called when it doesn't care about the stack trace. That said, if you want to trace all tasks, you can just ignore the cookies and record the traces.

Easy enough for the unwinder, but IIUC each tracer would have to maintain a global list of pending cookies (and corresponding ptrs to perf_event, trace_array, etc)? Would that not create a lot of contention?

Seems like there really needs to be some kind of per-task or per-request state.

Signature · 1 line
Josh
#125 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

In reply to Josh Poimboeuf
Quoted 13 lines · Josh Poimboeuf
include/linux/unwind_deferred.h +11 -0
 
+#ifdef CONFIG_UNWIND_USER
+
+void unwind_task_init(struct task_struct *task);
+void unwind_task_free(struct task_struct *task);
+
+void unwind_deferred_init(struct unwind_work *work, unwind_callback_t func);
+int unwind_deferred_request(struct unwind_work *work, u64 *cookie);
+bool unwind_deferred_cancel(struct task_struct *task, struct unwind_work *work);
+
+static __always_inline void unwind_enter_from_user_mode(void)
+{

include/linux/unwind_deferred.h line 39

Quoted 10 lines
include/linux/unwind_deferred.h +10 -0
+	current->unwind_info.cookie = 0;
+}
+
+#else /* !CONFIG_UNWIND_USER */
+
+static inline void unwind_task_init(struct task_struct *task) {}
+static inline void unwind_task_free(struct task_struct *task) {}
+
+static inline void unwind_deferred_init(struct unwind_work *work, unwind_callback_t func) {}
+static inline int unwind_deferred_request(struct task_struct *task, struct unwind_work *work, u64 *cookie) { return -ENOSYS; }

static inline int unwind_deferred_request(struct unwind_work *work, u64 *cookie) { return -ENOSYS; }

Otherwise this does not compile on architectures that do not enable UNWIND_USER.

include/linux/unwind_deferred.h line 44

Quoted 5 lines
include/linux/unwind_deferred.h +5 -0
+static inline bool unwind_deferred_cancel(struct task_struct *task, struct unwind_work *work) { return false; }
+
+static inline void unwind_enter_from_user_mode(void) {}
+
+#endif /* !CONFIG_UNWIND_USER */

Regards,
Jens

Signature · 9 lines
Jens Remus
Linux on Z Development (D3303)
+49-7031-16-1128 Office
[email protected]

IBM

IBM Deutschland Research & Development GmbH; Vorsitzender des Aufsichtsrats: Wolfgang Wendt; Geschäftsführung: David Faller; Sitz der Gesellschaft: Böblingen; Registergericht: Amtsgericht Stuttgart, HRB 243294
IBM Data Privacy Statement: https://www.ibm.com/privacy/
author maintainer
#126 lore raw

Re: [PATCH v4 28/39] unwind_user/deferred: Add deferred unwinding interface

Quoted 8 lines · Jens Remus

include/linux/unwind_deferred.h line 39

Quoted 2 lines · Josh Poimboeuf
include/linux/unwind_deferred.h new file +2 -0
+static inline void unwind_deferred_init(struct unwind_work *work, unwind_callback_t func) {}
+static inline int unwind_deferred_request(struct task_struct *task, struct unwind_work *work, u64 *cookie) { return -ENOSYS; }

static inline int unwind_deferred_request(struct unwind_work *work, u64 *cookie) { return -ENOSYS; }

Otherwise this does not compile on architectures that do not enable UNWIND_USER.

Yeah, bots have reported that also, thanks.

Signature · 1 line
Josh
author maintainer
#127 lore raw

PATCH v4 29/39 unwind_user/deferred: Add unwind cache

In reply to Josh Poimboeuf

Cache the results of the unwind to ensure the unwind is only performed once, even when called by multiple tracers.

---
 include/linux/unwind_deferred_types.h |  8 +++++++-
 kernel/unwind/deferred.c              | 26 ++++++++++++++++++++------
 2 files changed, 27 insertions(+), 7 deletions(-)
include/linux/unwind_deferred_types.h +7 -1
index 9749824aea09..6f71a06329fb 100644
@@ -2,8 +2,14 @@
 #ifndef _LINUX_UNWIND_USER_DEFERRED_TYPES_H
 #define _LINUX_UNWIND_USER_DEFERRED_TYPES_H
 
-struct unwind_task_info {
+struct unwind_cache {
 	unsigned long		*entries;
+	unsigned int		nr_entries;
+	u64			cookie;
+};
+
+struct unwind_task_info {
+	struct unwind_cache	cache;
 	u64			cookie;
 };
 
kernel/unwind/deferred.c +20 -6
index f0dbe4069247..2f38055cce48 100644
@@ -56,6 +56,7 @@ static void unwind_deferred_task_work(struct callback_head *head)
 {
 	struct unwind_work *work = container_of(head, struct unwind_work, work);
 	struct unwind_task_info *info = &current->unwind_info;
+	struct unwind_cache *cache = &info->cache;
 	struct unwind_stacktrace trace;
 	u64 cookie;
 
@@ -73,17 +74,30 @@ static void unwind_deferred_task_work(struct callback_head *head)
 	if (!current->mm)
 		goto do_callback;
 
-	if (!info->entries) {
-		info->entries = kmalloc(UNWIND_MAX_ENTRIES * sizeof(long),
-					GFP_KERNEL);
-		if (!info->entries)
+	if (!cache->entries) {
+		cache->entries = kmalloc(UNWIND_MAX_ENTRIES * sizeof(long),
+					 GFP_KERNEL);
+		if (!cache->entries)
 			goto do_callback;
 	}
 
-	trace.entries = info->entries;
+	trace.entries = cache->entries;
+
+	if (cookie == cache->cookie) {
+		/*
+		 * The user stack has already been previously unwound in this
+		 * entry context.  Skip the unwind and use the cache.
+		 */
+		trace.nr = cache->nr_entries;
+		goto do_callback;
+	}
+
 	trace.nr = 0;
 	unwind_user(&trace, UNWIND_MAX_ENTRIES);
 
+	cache->cookie = cookie;
+	cache->nr_entries = trace.nr;
+
 do_callback:
 	work->func(work, &trace, cookie);
 	work->pending = 0;
@@ -174,5 +188,5 @@ void unwind_task_free(struct task_struct *task)
 {
 	struct unwind_task_info *info = &task->unwind_info;
 
-	kfree(info->entries);
+	kfree(info->cache.entries);
 }
Signature · 1 line
2.48.1
maintainer
#128 lore raw

Re: [PATCH v4 29/39] unwind_user/deferred: Add unwind cache

Quoted 14 lines · Josh Poimboeuf

Cache the results of the unwind to ensure the unwind is only performed once, even when called by multiple tracers.

---
 include/linux/unwind_deferred_types.h |  8 +++++++-
 kernel/unwind/deferred.c              | 26 ++++++++++++++++++++------
 2 files changed, 27 insertions(+), 7 deletions(-)
include/linux/unwind_deferred_types.h +0 -0
index 9749824aea09..6f71a06329fb 100644

include/linux/unwind_deferred_types.h line 9

Quoted 9 lines
include/linux/unwind_deferred_types.h +4 -1
@@ -2,8 +2,14 @@
 #ifndef _LINUX_UNWIND_USER_DEFERRED_TYPES_H
 #define _LINUX_UNWIND_USER_DEFERRED_TYPES_H
 
-struct unwind_task_info {
+struct unwind_cache {
 	unsigned long		*entries;
+	unsigned int		nr_entries;
+	u64			cookie;
+};

If you make the return to user path clear nr_entries you don't need a second cookie field I think.

author maintainer
#129 lore raw

Re: [PATCH v4 29/39] unwind_user/deferred: Add unwind cache

Quoted 18 lines · Peter Zijlstra
Quoted 17 lines · Josh Poimboeuf

Cache the results of the unwind to ensure the unwind is only performed once, even when called by multiple tracers.

---
 include/linux/unwind_deferred_types.h |  8 +++++++-
 kernel/unwind/deferred.c              | 26 ++++++++++++++++++++------
 2 files changed, 27 insertions(+), 7 deletions(-)
include/linux/unwind_deferred_types.h +0 -0
index 9749824aea09..6f71a06329fb 100644
@@ -2,8 +2,14 @@
 #ifndef _LINUX_UNWIND_USER_DEFERRED_TYPES_H
 #define _LINUX_UNWIND_USER_DEFERRED_TYPES_H
 
Quoted 9 lines

include/linux/unwind_deferred_types.h line 9

Quoted 6 lines
include/linux/unwind_deferred_types.h +4 -1
-struct unwind_task_info {
+struct unwind_cache {
 	unsigned long		*entries;
+	unsigned int		nr_entries;
+	u64			cookie;
+};

If you make the return to user path clear nr_entries you don't need a second cookie field I think.

But if the NMI happens late in the exit-to-user path, with IRQs disabled, right before nr_entries gets cleared, the cache won't get used in the task work.

However I think we can clear it on entry-from-user.

Signature · 1 line
Josh
maintainer
#130 lore raw

Re: [PATCH v4 29/39] unwind_user/deferred: Add unwind cache

Quoted 29 lines · Josh Poimboeuf
Quoted 18 lines · Peter Zijlstra
Quoted 17 lines · Josh Poimboeuf

Cache the results of the unwind to ensure the unwind is only performed once, even when called by multiple tracers.

---
 include/linux/unwind_deferred_types.h |  8 +++++++-
 kernel/unwind/deferred.c              | 26 ++++++++++++++++++++------
 2 files changed, 27 insertions(+), 7 deletions(-)
include/linux/unwind_deferred_types.h +0 -0
index 9749824aea09..6f71a06329fb 100644
@@ -2,8 +2,14 @@
 #ifndef _LINUX_UNWIND_USER_DEFERRED_TYPES_H
 #define _LINUX_UNWIND_USER_DEFERRED_TYPES_H
 
Quoted 9 lines

include/linux/unwind_deferred_types.h line 9

Quoted 6 lines
include/linux/unwind_deferred_types.h +4 -1
-struct unwind_task_info {
+struct unwind_cache {
 	unsigned long		*entries;
+	unsigned int		nr_entries;
+	u64			cookie;
+};

If you make the return to user path clear nr_entries you don't need a second cookie field I think.

Quoted 5 lines

But if the NMI happens late in the exit-to-user path, with IRQs disabled, right before nr_entries gets cleared, the cache won't get used in the task work.

However I think we can clear it on entry-from-user.

Return to user runs with interrupts disabled, if an NMI hits that, it will have to set TIF_NOTIFY_RESUME again and queue the IRQ work thing. That self-IPI will hit the moment we do IRET (which is what re-enables interrupts) and we're going back into the kernel.

Anyway, I suppose that is a long way of saying that you should be able to do this on return to user.

But yes, enter-from-user should work too.

author maintainer
#131 lore raw

Re: [PATCH v4 29/39] unwind_user/deferred: Add unwind cache

Quoted 9 lines · Peter Zijlstra
Quoted 7 lines · Josh Poimboeuf
Quoted 1 line · Peter Zijlstra

On Tue, Jan 21, 2025 at 06:31:21PM -0800, Josh Poimboeuf wrote:

But if the NMI happens late in the exit-to-user path, with IRQs disabled, right before nr_entries gets cleared, the cache won't get used in the task work.

However I think we can clear it on entry-from-user.

Quoted 7 lines

Return to user runs with interrupts disabled, if an NMI hits that, it will have to set TIF_NOTIFY_RESUME again and queue the IRQ work thing. That self-IPI will hit the moment we do IRET (which is what re-enables interrupts) and we're going back into the kernel.

Anyway, I suppose that is a long way of saying that you should be able to do this on return to user.

Indeed, I knew that but somehow overlooked the fact that the IRQ would clear the cookie so the cache wouldn't be usable anyway.

Signature · 1 line
Josh
author maintainer
#132 lore raw

PATCH v4 30/39 unwind_user/deferred: Make unwind deferral requests NMI-safe

In reply to Josh Poimboeuf

Make unwind_deferred_request() NMI-safe so tracers in NMI context can call it to get the cookie immediately rather than have to do the fragile "schedule irq work and then call unwind_deferred_request()" dance.

---
 include/linux/entry-common.h          |   1 +
 include/linux/unwind_deferred.h       |   6 ++
 include/linux/unwind_deferred_types.h |   1 +
 kernel/unwind/deferred.c              | 106 ++++++++++++++++++++++----
 4 files changed, 98 insertions(+), 16 deletions(-)
include/linux/entry-common.h +1 -0
index fb2b27154fee..e9b8c145f480 100644
@@ -363,6 +363,7 @@ static __always_inline void exit_to_user_mode(void)
 	lockdep_hardirqs_on_prepare();
 	instrumentation_end();
 
+	unwind_exit_to_user_mode();
 	user_enter_irqoff();
 	arch_exit_to_user_mode();
 	lockdep_hardirqs_on(CALLER_ADDR0);
include/linux/unwind_deferred.h +6 -0
index 741f409f0d1f..22269f4d2392 100644
@@ -30,6 +30,11 @@ static __always_inline void unwind_enter_from_user_mode(void)
 	current->unwind_info.cookie = 0;
 }
 
+static __always_inline void unwind_exit_to_user_mode(void)
+{
+	current->unwind_info.cookie = 0;
+}
+
 #else /* !CONFIG_UNWIND_USER */
 
 static inline void unwind_task_init(struct task_struct *task) {}
@@ -40,6 +45,7 @@ static inline int unwind_deferred_request(struct task_struct *task, struct unwin
 static inline bool unwind_deferred_cancel(struct task_struct *task, struct unwind_work *work) { return false; }
 
 static inline void unwind_enter_from_user_mode(void) {}
+static inline void unwind_exit_to_user_mode(void) {}
 
 #endif /* !CONFIG_UNWIND_USER */
 
include/linux/unwind_deferred_types.h +1 -0
index 6f71a06329fb..c535cca6534b 100644
@@ -11,6 +11,7 @@ struct unwind_cache {
 struct unwind_task_info {
 	struct unwind_cache	cache;
 	u64			cookie;
+	u64			nmi_cookie;
 };
 
 #endif /* _LINUX_UNWIND_USER_DEFERRED_TYPES_H */
kernel/unwind/deferred.c +90 -16

153 lines not on this page · raw message

Signature · 1 line
2.48.1
maintainer
#133 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

Quoted 49 lines · Josh Poimboeuf
kernel/unwind/deferred.c +23 -6
index 2f38055cce48..939c94abaa50 100644
@@ -29,27 +29,49 @@ static u64 ctx_to_cookie(u64 cpu, u64 ctx)
 
 /*
  * Read the task context cookie, first initializing it if this is the first
- * call to get_cookie() since the most recent entry from user.
+ * call to get_cookie() since the most recent entry from user.  This has to be
+ * done carefully to coordinate with unwind_deferred_request_nmi().
  */
 static u64 get_cookie(struct unwind_task_info *info)
 {
 	u64 ctx_ctr;
 	u64 cookie;
-	u64 cpu;
 
 	guard(irqsave)();
 
-	cookie = info->cookie;
+	cookie = READ_ONCE(info->cookie);
 	if (cookie)
 		return cookie;
 
+	ctx_ctr = __this_cpu_read(unwind_ctx_ctr);
 
-	cpu = raw_smp_processor_id();
-	ctx_ctr = __this_cpu_inc_return(unwind_ctx_ctr);
-	info->cookie = ctx_to_cookie(cpu, ctx_ctr);
+	/* Read ctx_ctr before info->nmi_cookie */
+	barrier();
+
+	cookie = READ_ONCE(info->nmi_cookie);
+	if (cookie) {
+		/*
+		 * This is the first call to get_cookie() since an NMI handler
+		 * first wrote it to info->nmi_cookie.  Sync it.
+		 */
+		WRITE_ONCE(info->cookie, cookie);
+		WRITE_ONCE(info->nmi_cookie, 0);
+		return cookie;
+	}
+
+	/*
+	 * Write info->cookie.  It's ok to race with an NMI here.  The value of
+	 * the cookie is based on ctx_ctr from before the NMI could have
+	 * incremented it.  The result will be the same even if cookie or
+	 * ctx_ctr end up getting written twice.

kernel/unwind/deferred.c static u64 ctx_to_cookie(u64 cpu, u64 ctx) · line 75

Quoted 10 lines
kernel/unwind/deferred.c +6 -1
+	 */
+	cookie = ctx_to_cookie(raw_smp_processor_id(), ctx_ctr + 1);
+	WRITE_ONCE(info->cookie, cookie);
+	WRITE_ONCE(info->nmi_cookie, 0);
+	barrier();
+	__this_cpu_write(unwind_ctx_ctr, ctx_ctr + 1);
 
 	return cookie;
-
 }

Oh gawd. Can we please do something simple like:

	guard(irqsave)();
	cpu = raw_smp_processor_id();
	ctr = __this_cpu_read(unwind_ctx_cnt);
	cookie = READ_ONCE(current->unwind_info.cookie);
	do {
		if (cookie)
			return cookie;
		cookie = ctx_to_cookie(cpu, ctr+1);
	} while (!try_cmpxchg64(&current->unwind_info.cookie, &cookie, cookie));
	__this_cpu_write(unwind_ctx_ctr, ctr+1);
	return cookie;
author maintainer
#134 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

Quoted 6 lines · Peter Zijlstra

On Tue, Jan 21, 2025 at 06:31:22PM -0800, Josh Poimboeuf wrote:
Oh gawd. Can we please do something simple like:

	guard(irqsave)();
	cpu = raw_smp_processor_id();
	ctr = __this_cpu_read(unwind_ctx_cnt);

Don't you need a compiler barrier here? __this_cpu_read() doesn't have one.

Quoted 8 lines
	cookie = READ_ONCE(current->unwind_info.cookie);
	do {
		if (cookie)
			return cookie;
		cookie = ctx_to_cookie(cpu, ctr+1);
	} while (!try_cmpxchg64(&current->unwind_info.cookie, &cookie, cookie));
	__this_cpu_write(unwind_ctx_ctr, ctr+1);
	return cookie;

I was trying to avoid the overhead of the cmpxchg.

But also, the nmi_cookie is still needed for the case where the NMI arrives before info->cookie gets cleared by early entry-from-user.

Signature · 1 line
Josh
maintainer
#135 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

Quoted 10 lines · Josh Poimboeuf
Quoted 6 lines · Peter Zijlstra

On Tue, Jan 21, 2025 at 06:31:22PM -0800, Josh Poimboeuf wrote:
Oh gawd. Can we please do something simple like:

	guard(irqsave)();
	cpu = raw_smp_processor_id();
	ctr = __this_cpu_read(unwind_ctx_cnt);

Don't you need a compiler barrier here? __this_cpu_read() doesn't have one.

What for?

Quoted 10 lines
Quoted 8 lines
	cookie = READ_ONCE(current->unwind_info.cookie);
	do {
		if (cookie)
			return cookie;
		cookie = ctx_to_cookie(cpu, ctr+1);
	} while (!try_cmpxchg64(&current->unwind_info.cookie, &cookie, cookie));
	__this_cpu_write(unwind_ctx_ctr, ctr+1);
	return cookie;

I was trying to avoid the overhead of the cmpxchg.

We're going to be doing userspace stack unwinding, I don't think overhead is a real concern.

Quoted 2 lines

But also, the nmi_cookie is still needed for the case where the NMI arrives before info->cookie gets cleared by early entry-from-user.

So how about we clear cookie (and set nr_entries to -1) at return-to-user, after we've done the work loop and have interrupts disabled until we hit userspace.

Any NMI that hits there will have to cause another entry anyway.

author maintainer
#136 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

Quoted 5 lines · Peter Zijlstra
Quoted 4 lines · Josh Poimboeuf
Quoted 3 lines · Peter Zijlstra

On Tue, Jan 21, 2025 at 06:31:22PM -0800, Josh Poimboeuf wrote:
Oh gawd. Can we please do something simple like:

Quoted 8 lines
Quoted 6 lines
Quoted 3 lines
	guard(irqsave)();
	cpu = raw_smp_processor_id();
	ctr = __this_cpu_read(unwind_ctx_cnt);

Don't you need a compiler barrier here? __this_cpu_read() doesn't have one.

What for?

Hm, I guess it's not needed for this one.

Quoted 6 lines
Quoted 6 lines
Quoted 6 lines
	cookie = READ_ONCE(current->unwind_info.cookie);
	do {
		if (cookie)
			return cookie;
		cookie = ctx_to_cookie(cpu, ctr+1);
	} while (!try_cmpxchg64(&current->unwind_info.cookie, &cookie, cookie));

Should not the 2nd argument be &zero?

Quoted 6 lines
Quoted 4 lines
Quoted 2 lines
	__this_cpu_write(unwind_ctx_ctr, ctr+1);
	return cookie;

But also, the nmi_cookie is still needed for the case where the NMI arrives before info->cookie gets cleared by early entry-from-user.

So how about we clear cookie (and set nr_entries to -1) at

I think we could set nr_entries to 0 instead of -1?

Quoted 4 lines

return-to-user, after we've done the work loop and have interrupts disabled until we hit userspace.

Any NMI that hits there will have to cause another entry anyway.

But there's a cookie mismatch:

    // return-to-user: IRQs disabled
    <NMI>
	current->unwind_info.cookie = 0x1234
    </NMI>
    unwind_exit_to_user_mode()
	current->unwind_info.cookie = 0
    IRET
<IRQ>
    task_work()
        callback(@cookie=WRONG)
Signature · 1 line
Josh
author maintainer
#137 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

Quoted 16 lines · Josh Poimboeuf
Quoted 5 lines · Peter Zijlstra
Quoted 2 lines · Josh Poimboeuf

But also, the nmi_cookie is still needed for the case where the NMI arrives before info->cookie gets cleared by early entry-from-user.

So how about we clear cookie (and set nr_entries to -1) at

I think we could set nr_entries to 0 instead of -1?

Quoted 4 lines

return-to-user, after we've done the work loop and have interrupts disabled until we hit userspace.

Any NMI that hits there will have to cause another entry anyway.

But there's a cookie mismatch:

Quoted 10 lines
    // return-to-user: IRQs disabled
    <NMI>
	current->unwind_info.cookie = 0x1234
    </NMI>
    unwind_exit_to_user_mode()
	current->unwind_info.cookie = 0
    IRET
<IRQ>
    task_work()
        callback(@cookie=WRONG)

Though, assuming we're keeping the unwind_work struct, there's a simpler alternative to nmi_cookie: store the cookie in the unwind_work. Then the task work can just use that instead of current->unwind_info.cookie.

Signature · 1 line
Josh
maintainer
#138 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

In reply to Josh Poimboeuf
Quoted 8 lines · Josh Poimboeuf
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines
	cookie = READ_ONCE(current->unwind_info.cookie);
	do {
		if (cookie)
			return cookie;
		cookie = ctx_to_cookie(cpu, ctr+1);
	} while (!try_cmpxchg64(&current->unwind_info.cookie, &cookie, cookie));

Should not the 2nd argument be &zero?

This I suppose

	cookie = READ_ONCE(current->unwind_info.cookie);
	do {
		if (cookie)
			return cookie;
	} while (!try_cmpxchg64(&current->unwind_info.cookie, &cookie,
				ctx_to_cookie(cpu, ctr+1)));
author maintainer
#139 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

Quoted 10 lines · Peter Zijlstra
Quoted 8 lines · Josh Poimboeuf
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines
	cookie = READ_ONCE(current->unwind_info.cookie);
	do {
		if (cookie)
			return cookie;
		cookie = ctx_to_cookie(cpu, ctr+1);
	} while (!try_cmpxchg64(&current->unwind_info.cookie, &cookie, cookie));

Should not the 2nd argument be &zero?

Quoted 8 lines

This I suppose

	cookie = READ_ONCE(current->unwind_info.cookie);
	do {
		if (cookie)
			return cookie;
	} while (!try_cmpxchg64(&current->unwind_info.cookie, &cookie,
				ctx_to_cookie(cpu, ctr+1)));

Here's what I have at the moment. unwind_deferred_request() is vastly simplified thanks to your suggestions, no more NMI-specific muck. work->pending is replaced with work->cookie so the task work can use it in case it crossed the cookie-clearing boundary (return-to-user with IRQs disabled).

static inline bool nmi_supported(void)

{
	return IS_ENABLED(CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG);
}

/*
* Read the task context cookie, initializing it if this is the first request
* since the most recent entry from user.
*/
static u64 get_cookie(void)

{
	struct unwind_task_info *info = &current->unwind_info;
	u64 ctr, cookie, old_cookie = 0;
	unsigned int cpu;

	cookie = READ_ONCE(info->cookie);
	if (cookie)
		return cookie;

	guard(irqsave)();

	cpu = raw_smp_processor_id();
	ctr = __this_cpu_read(unwind_ctx_ctr);

	cookie = ctx_to_cookie(cpu, ++ctr);

	/* Make sure the cookie is non-zero. */
	if (!cookie)
		cookie = ctx_to_cookie(cpu, ++ctr);

	/* Write the task cookie unless an NMI just now swooped in to do so. */
	if (!try_cmpxchg64(&info->cookie, &old_cookie, cookie))
		return old_cookie;

	/* Success, update the context counter. */
	__this_cpu_write(unwind_ctx_ctr, ctr);

	return cookie;
}

/*
* Schedule a user space unwind to be done in task work before exiting the
* kernel.
*
* The returned cookie output is a unique identifer for the current task entry
* context. Its value will also be passed to the callback function. It can be
* used to stitch kernel and user stack traces together in post-processing. *
* It's valid to call this function multiple times for the same @work within
* the same task entry context. Each call will return the same cookie. If the
* callback is already pending, an error will be returned along with the
* cookie. If the callback is not pending because it has already been
* previously called for the same entry context, it will be called again with
* the same stack trace and cookie.
*
* Thus are three possible return scenarios:
*
* * return != 0, *cookie == 0: the operation failed, no pending callback.
*
* * return != 0, *cookie != 0: the callback is already pending. The cookie
* can still be used to correlate with the pending callback.
*
* * return == 0, *cookie != 0: the callback queued successfully. The
* callback is guaranteed to be called with the given cookie.
*/
int unwind_deferred_request(struct unwind_work *work, u64 *cookie)

{
	enum task_work_notify_mode mode = in_nmi() ? TWA_NMI_CURRENT : TWA_RESUME;
	u64 zero = 0;
	int ret;

	*cookie = 0;

	if (!current->mm || !user_mode(task_pt_regs(current)))
		return -EINVAL;

	if (!nmi_supported() && in_nmi())
		return -EINVAL;

	/* Return a valid @cookie even if the callback is already pending. */
	*cookie = READ_ONCE(work->cookie);
	if (*cookie)
		return -EEXIST;

	*cookie = get_cookie();

	/* Claim the work unless an NMI just now swooped in to do so. */
	if (!try_cmpxchg64(&work->cookie, &zero, *cookie))
		return -EEXIST;

	/* The work has been claimed, now schedule it. */
	ret = task_work_add(current, &work->work, mode);
	if (WARN_ON_ONCE(ret)) {
		WRITE_ONCE(work->cookie, 0);
		return ret;
	}

	return 0;
}
Signature · 1 line
Josh
maintainer
#140 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

Quoted 5 lines · Josh Poimboeuf

Here's what I have at the moment. unwind_deferred_request() is vastly simplified thanks to your suggestions, no more NMI-specific muck. work->pending is replaced with work->cookie so the task work can use it in case it crossed the cookie-clearing boundary (return-to-user with IRQs disabled).

That looks a lot saner, thanks! I'll be waiting for the next cycle, not sure I can track all the changes in me head.

maintainer
#141 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

In reply to Josh Poimboeuf
Quoted 12 lines · Josh Poimboeuf
kernel/unwind/deferred.c +12 -0
+static int unwind_deferred_request_nmi(struct unwind_work *work, u64 *cookie)
+{
+	struct unwind_task_info *info = &current->unwind_info;
+	bool inited_cookie = false;
+	int ret;
+
+	*cookie = info->cookie;
+	if (!*cookie) {
+		/*
+		 * This is the first unwind request since the most recent entry
+		 * from user.  Initialize the task cookie.
+		 *

kernel/unwind/deferred.c static void unwind_deferred_task_work(struct callback_head *head) · line 149

Quoted 10 lines
kernel/unwind/deferred.c +10 -0
+		 * Don't write to info->cookie directly, otherwise it may get
+		 * cleared if the NMI occurred in the kernel during early entry
+		 * or late exit before the task work gets to run.  Instead, use
+		 * info->nmi_cookie which gets synced later by get_cookie().
+		 */
+		if (!info->nmi_cookie) {
+			u64 cpu = raw_smp_processor_id();
+			u64 ctx_ctr;
+
+			ctx_ctr = __this_cpu_inc_return(unwind_ctx_ctr);

__this_cpu_inc_return() is *NOT* NMI safe IIRC.

author maintainer
#142 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

Quoted 17 lines · Peter Zijlstra
Quoted 16 lines · Josh Poimboeuf
kernel/unwind/deferred.c +16 -0
+static int unwind_deferred_request_nmi(struct unwind_work *work, u64 *cookie)
+{
+	struct unwind_task_info *info = &current->unwind_info;
+	bool inited_cookie = false;
+	int ret;
+
+	*cookie = info->cookie;
+	if (!*cookie) {
+		/*
+		 * This is the first unwind request since the most recent entry
+		 * from user.  Initialize the task cookie.
+		 *
+		 * Don't write to info->cookie directly, otherwise it may get
+		 * cleared if the NMI occurred in the kernel during early entry
+		 * or late exit before the task work gets to run.  Instead, use
+		 * info->nmi_cookie which gets synced later by get_cookie().
Quoted 8 lines

kernel/unwind/deferred.c static void unwind_deferred_task_work(struct callback_head *head) · line 149

Quoted 6 lines
kernel/unwind/deferred.c +6 -0
+		 */
+		if (!info->nmi_cookie) {
+			u64 cpu = raw_smp_processor_id();
+			u64 ctx_ctr;
+
+			ctx_ctr = __this_cpu_inc_return(unwind_ctx_ctr);

__this_cpu_inc_return() is *NOT* NMI safe IIRC.

Hm, I guess I was only looking at x86.

Signature · 1 line
Josh
maintainer
#143 lore raw

Re: [PATCH v4 30/39] unwind_user/deferred: Make unwind deferral requests NMI-safe

Quoted 27 lines · Josh Poimboeuf
Quoted 17 lines · Peter Zijlstra
Quoted 16 lines · Josh Poimboeuf
kernel/unwind/deferred.c +16 -0
+static int unwind_deferred_request_nmi(struct unwind_work *work, u64 *cookie)
+{
+	struct unwind_task_info *info = &current->unwind_info;
+	bool inited_cookie = false;
+	int ret;
+
+	*cookie = info->cookie;
+	if (!*cookie) {
+		/*
+		 * This is the first unwind request since the most recent entry
+		 * from user.  Initialize the task cookie.
+		 *
+		 * Don't write to info->cookie directly, otherwise it may get
+		 * cleared if the NMI occurred in the kernel during early entry
+		 * or late exit before the task work gets to run.  Instead, use
+		 * info->nmi_cookie which gets synced later by get_cookie().
Quoted 8 lines

kernel/unwind/deferred.c static void unwind_deferred_task_work(struct callback_head *head) · line 149

Quoted 6 lines
kernel/unwind/deferred.c +6 -0
+		 */
+		if (!info->nmi_cookie) {
+			u64 cpu = raw_smp_processor_id();
+			u64 ctx_ctr;
+
+			ctx_ctr = __this_cpu_inc_return(unwind_ctx_ctr);

__this_cpu_inc_return() is *NOT* NMI safe IIRC.

Quoted 1 line

Hm, I guess I was only looking at x86.

:-), right so x86 is rather special here, the various RISC platforms are what you should be looking at, eg. arm64.

author maintainer
#144 lore raw

PATCH v4 31/39 perf: Remove get_perf_callchain() 'init_nr' argument

In reply to Josh Poimboeuf

The 'init_nr' argument has double duty: it's used to initialize both the number of contexts and the number of stack entries. That's confusing and the callers always pass zero anyway. Hard code the zero.

---
 include/linux/perf_event.h |  2 +-
 kernel/bpf/stackmap.c      |  4 ++--
 kernel/events/callchain.c  | 12 ++++++------
 kernel/events/core.c       |  2 +-
 4 files changed, 10 insertions(+), 10 deletions(-)
include/linux/perf_event.h +1 -1
index cb99ec8c9e96..4c8ff7258c6a 100644
@@ -1589,7 +1589,7 @@ DECLARE_PER_CPU(struct perf_callchain_entry, perf_callchain_entry);
 extern void perf_callchain_user(struct perf_callchain_entry_ctx *entry, struct pt_regs *regs);
 extern void perf_callchain_kernel(struct perf_callchain_entry_ctx *entry, struct pt_regs *regs);
 extern struct perf_callchain_entry *
-get_perf_callchain(struct pt_regs *regs, u32 init_nr, bool kernel, bool user,
+get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
 		   u32 max_stack, bool crosstask, bool add_mark);
 extern int get_callchain_buffers(int max_stack);
 extern void put_callchain_buffers(void);
kernel/bpf/stackmap.c +2 -2
index 3615c06b7dfa..ec3a57a5fba1 100644
@@ -314,7 +314,7 @@ BPF_CALL_3(bpf_get_stackid, struct pt_regs *, regs, struct bpf_map *, map,
 	if (max_depth > sysctl_perf_event_max_stack)
 		max_depth = sysctl_perf_event_max_stack;
 
-	trace = get_perf_callchain(regs, 0, kernel, user, max_depth,
+	trace = get_perf_callchain(regs, kernel, user, max_depth,
 				   false, false);
 
 	if (unlikely(!trace))
@@ -451,7 +451,7 @@ static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task,
 	else if (kernel && task)
 		trace = get_callchain_entry_for_task(task, max_depth);
 	else
-		trace = get_perf_callchain(regs, 0, kernel, user, max_depth,
+		trace = get_perf_callchain(regs, kernel, user, max_depth,
 					   crosstask, false);
 
 	if (unlikely(!trace) || trace->nr < skip) {
kernel/events/callchain.c +6 -6
index 8a47e52a454f..83834203e144 100644
@@ -216,7 +216,7 @@ static void fixup_uretprobe_trampoline_entries(struct perf_callchain_entry *entr
 }
 
 struct perf_callchain_entry *
-get_perf_callchain(struct pt_regs *regs, u32 init_nr, bool kernel, bool user,
+get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
 		   u32 max_stack, bool crosstask, bool add_mark)
 {
 	struct perf_callchain_entry *entry;
@@ -227,11 +227,11 @@ get_perf_callchain(struct pt_regs *regs, u32 init_nr, bool kernel, bool user,
 	if (!entry)
 		return NULL;
 
-	ctx.entry     = entry;
-	ctx.max_stack = max_stack;
-	ctx.nr	      = entry->nr = init_nr;
-	ctx.contexts       = 0;
-	ctx.contexts_maxed = false;
+	ctx.entry		= entry;
+	ctx.max_stack		= max_stack;
+	ctx.nr			= entry->nr = 0;
+	ctx.contexts		= 0;
+	ctx.contexts_maxed	= false;
 
 	if (kernel && !user_mode(regs)) {
 		if (add_mark)
kernel/events/core.c +1 -1
index 065f9188b44a..ebe457bacf96 100644
@@ -7801,7 +7801,7 @@ perf_callchain(struct perf_event *event, struct pt_regs *regs)
 	if (!kernel && !user)
 		return &__empty_callchain;
 
-	callchain = get_perf_callchain(regs, 0, kernel, user,
+	callchain = get_perf_callchain(regs, kernel, user,
 				       max_stack, crosstask, true);
 	return callchain ?: &__empty_callchain;
 }
Signature · 1 line
2.48.1
author maintainer
#145 lore raw

PATCH v4 32/39 perf: Remove get_perf_callchain() 'crosstask' argument

In reply to Josh Poimboeuf

get_perf_callchain() doesn't support cross-task unwinding, so it doesn't make much sense to have 'crosstask' as an argument.

---
 include/linux/perf_event.h |  2 +-
 kernel/bpf/stackmap.c      | 12 ++++--------
 kernel/events/callchain.c  |  6 +-----
 kernel/events/core.c       |  9 +++++----
 4 files changed, 11 insertions(+), 18 deletions(-)
include/linux/perf_event.h +1 -1
index 4c8ff7258c6a..1563dc2cd979 100644
@@ -1590,7 +1590,7 @@ extern void perf_callchain_user(struct perf_callchain_entry_ctx *entry, struct p
 extern void perf_callchain_kernel(struct perf_callchain_entry_ctx *entry, struct pt_regs *regs);
 extern struct perf_callchain_entry *
 get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
-		   u32 max_stack, bool crosstask, bool add_mark);
+		   u32 max_stack, bool add_mark);
 extern int get_callchain_buffers(int max_stack);
 extern void put_callchain_buffers(void);
 extern struct perf_callchain_entry *get_callchain_entry(int *rctx);
kernel/bpf/stackmap.c +4 -8
index ec3a57a5fba1..ee9701337912 100644
@@ -314,8 +314,7 @@ BPF_CALL_3(bpf_get_stackid, struct pt_regs *, regs, struct bpf_map *, map,
 	if (max_depth > sysctl_perf_event_max_stack)
 		max_depth = sysctl_perf_event_max_stack;
 
-	trace = get_perf_callchain(regs, kernel, user, max_depth,
-				   false, false);
+	trace = get_perf_callchain(regs, kernel, user, max_depth, false);
 
 	if (unlikely(!trace))
 		/* couldn't fetch the stack trace */
@@ -430,10 +429,8 @@ static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task,
 	if (task && user && !user_mode(regs))
 		goto err_fault;
 
-	/* get_perf_callchain does not support crosstask user stack walking
-	 * but returns an empty stack instead of NULL.
-	 */
-	if (crosstask && user) {
+	/* get_perf_callchain() does not support crosstask stack walking */
+	if (crosstask) {
 		err = -EOPNOTSUPP;
 		goto clear;
 	}
@@ -451,8 +448,7 @@ static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task,
 	else if (kernel && task)
 		trace = get_callchain_entry_for_task(task, max_depth);
 	else
-		trace = get_perf_callchain(regs, kernel, user, max_depth,
-					   crosstask, false);
+		trace = get_perf_callchain(regs, kernel, user, max_depth,false);
 
 	if (unlikely(!trace) || trace->nr < skip) {
 		if (may_fault)
kernel/events/callchain.c +1 -5
index 83834203e144..655fb25a725b 100644
@@ -217,7 +217,7 @@ static void fixup_uretprobe_trampoline_entries(struct perf_callchain_entry *entr
 
 struct perf_callchain_entry *
 get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
-		   u32 max_stack, bool crosstask, bool add_mark)
+		   u32 max_stack, bool add_mark)
 {
 	struct perf_callchain_entry *entry;
 	struct perf_callchain_entry_ctx ctx;
@@ -248,9 +248,6 @@ get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
 		}
 
 		if (regs) {
-			if (crosstask)
-				goto exit_put;
-
 			if (add_mark)
 				perf_callchain_store_context(&ctx, PERF_CONTEXT_USER);
 
@@ -260,7 +257,6 @@ get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
 		}
 	}
 
-exit_put:
 	put_callchain_entry(rctx);
 
 	return entry;
kernel/events/core.c +5 -4
index ebe457bacf96..99f0f28feeb5 100644
@@ -7793,16 +7793,17 @@ perf_callchain(struct perf_event *event, struct pt_regs *regs)
 {
 	bool kernel = !event->attr.exclude_callchain_kernel;
 	bool user   = !event->attr.exclude_callchain_user;
-	/* Disallow cross-task user callchains. */
-	bool crosstask = event->ctx->task && event->ctx->task != current;
 	const u32 max_stack = event->attr.sample_max_stack;
 	struct perf_callchain_entry *callchain;
 
 	if (!kernel && !user)
 		return &__empty_callchain;
 
-	callchain = get_perf_callchain(regs, kernel, user,
-				       max_stack, crosstask, true);
+	/* Disallow cross-task callchains. */
+	if (event->ctx->task && event->ctx->task != current)
+		return &__empty_callchain;
+
+	callchain = get_perf_callchain(regs, kernel, user, max_stack, true);
 	return callchain ?: &__empty_callchain;
 }
 
Signature · 1 line
2.48.1
#146 lore raw

Re: [PATCH v4 32/39] perf: Remove get_perf_callchain() 'crosstask' argument

Quoted 41 lines · Josh Poimboeuf

get_perf_callchain() doesn't support cross-task unwinding, so it doesn't make much sense to have 'crosstask' as an argument.

---
 include/linux/perf_event.h |  2 +-
 kernel/bpf/stackmap.c      | 12 ++++--------
 kernel/events/callchain.c  |  6 +-----
 kernel/events/core.c       |  9 +++++----
 4 files changed, 11 insertions(+), 18 deletions(-)
include/linux/perf_event.h +1 -1
index 4c8ff7258c6a..1563dc2cd979 100644
@@ -1590,7 +1590,7 @@ extern void perf_callchain_user(struct perf_callchain_entry_ctx *entry, struct p
 extern void perf_callchain_kernel(struct perf_callchain_entry_ctx *entry, struct pt_regs *regs);
 extern struct perf_callchain_entry *
 get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
-                  u32 max_stack, bool crosstask, bool add_mark);
+                  u32 max_stack, bool add_mark);
 extern int get_callchain_buffers(int max_stack);
 extern void put_callchain_buffers(void);
 extern struct perf_callchain_entry *get_callchain_entry(int *rctx);
kernel/bpf/stackmap.c +1 -2
index ec3a57a5fba1..ee9701337912 100644
@@ -314,8 +314,7 @@ BPF_CALL_3(bpf_get_stackid, struct pt_regs *, regs, struct bpf_map *, map,
        if (max_depth > sysctl_perf_event_max_stack)
                max_depth = sysctl_perf_event_max_stack;
 
-       trace = get_perf_callchain(regs, kernel, user, max_depth,
-                                  false, false);
+       trace = get_perf_callchain(regs, kernel, user, max_depth, false);
 
        if (unlikely(!trace))
                /* couldn't fetch the stack trace */

kernel/bpf/stackmap.c static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task, · line 433

Quoted 9 lines
kernel/bpf/stackmap.c +2 -4
@@ -430,10 +429,8 @@ static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task,
        if (task && user && !user_mode(regs))
                goto err_fault;
 
-       /* get_perf_callchain does not support crosstask user stack walking
-        * but returns an empty stack instead of NULL.
-        */
-       if (crosstask && user) {
+       /* get_perf_callchain() does not support crosstask stack walking */
+       if (crosstask) {

crosstask stack trace is supported for kernel stack traces (see get_callchain_entry_for_task() call), so this is breaking that case

kernel/bpf/stackmap.c static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task, · line 451

Quoted 10 lines
kernel/bpf/stackmap.c +1 -2
                err = -EOPNOTSUPP;
                goto clear;
        }
@@ -451,8 +448,7 @@ static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task,
        else if (kernel && task)
                trace = get_callchain_entry_for_task(task, max_depth);
        else
-               trace = get_perf_callchain(regs, kernel, user, max_depth,
-                                          crosstask, false);
+               trace = get_perf_callchain(regs, kernel, user, max_depth,false);

nit: missing space

Quoted 7 lines
kernel/bpf/stackmap.c +0 -0
        if (unlikely(!trace) || trace->nr < skip) {
                if (may_fault)
kernel/events/callchain.c +0 -0
index 83834203e144..655fb25a725b 100644

[...]

author maintainer
#147 lore raw

Re: [PATCH v4 32/39] perf: Remove get_perf_callchain() 'crosstask' argument

Quoted 5 lines · Andrii Nakryiko
Quoted 4 lines · Josh Poimboeuf
kernel/bpf/stackmap.c +0 -0
@@ -430,10 +429,8 @@ static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task,
        if (task && user && !user_mode(regs))
                goto err_fault;
 
Quoted 9 lines

kernel/bpf/stackmap.c static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task, · line 433

Quoted 6 lines
kernel/bpf/stackmap.c +2 -4
-       /* get_perf_callchain does not support crosstask user stack walking
-        * but returns an empty stack instead of NULL.
-        */
-       if (crosstask && user) {
+       /* get_perf_callchain() does not support crosstask stack walking */
+       if (crosstask) {

crosstask stack trace is supported for kernel stack traces (see get_callchain_entry_for_task() call), so this is breaking that case

Oh I see, thanks.

BTW, that seems dubious, does it do anything to ensure the task isn't
running? Otherwise the unwind is going to be a wild ride.

Signature · 1 line
Josh
#148 lore raw

Re: [PATCH v4 32/39] perf: Remove get_perf_callchain() 'crosstask' argument

Quoted 17 lines · Josh Poimboeuf
Quoted 5 lines · Andrii Nakryiko
Quoted 4 lines · Josh Poimboeuf
kernel/bpf/stackmap.c +0 -0
@@ -430,10 +429,8 @@ static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task,
        if (task && user && !user_mode(regs))
                goto err_fault;
 
Quoted 9 lines

kernel/bpf/stackmap.c static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task, · line 433

Quoted 6 lines
kernel/bpf/stackmap.c +2 -4
-       /* get_perf_callchain does not support crosstask user stack walking
-        * but returns an empty stack instead of NULL.
-        */
-       if (crosstask && user) {
+       /* get_perf_callchain() does not support crosstask stack walking */
+       if (crosstask) {

crosstask stack trace is supported for kernel stack traces (see get_callchain_entry_for_task() call), so this is breaking that case

Quoted 4 lines

Oh I see, thanks.

BTW, that seems dubious, does it do anything to ensure the task isn't
running? Otherwise the unwind is going to be a wild ride.

Yeah, I think it's very speculative and doesn't pause the task in any way (just makes sure it doesn't go away). We just rely on stack_trace_save_tsk() -> arch_stack_walk(), which just optimistically tries to unwind, it seems.

It's still useful and if the user is prepared to handle a potentially garbage stack trace, why not. People do similar thing for user space stack trace (with custom BPF code), and it's very useful (even if not "reliable" by any means).

Quoted 3 lines

--
Josh

author maintainer
#149 lore raw

PATCH v4 33/39 perf: Simplify get_perf_callchain() user logic

In reply to Josh Poimboeuf

Simplify the get_perf_callchain() user logic a bit. task_pt_regs() should never be NULL.

---
 kernel/events/callchain.c | 20 +++++++++-----------
 1 file changed, 9 insertions(+), 11 deletions(-)
kernel/events/callchain.c +9 -11
index 655fb25a725b..2278402b7ac9 100644
@@ -241,22 +241,20 @@ get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
 
 	if (user) {
 		if (!user_mode(regs)) {
-			if  (current->mm)
-				regs = task_pt_regs(current);
-			else
-				regs = NULL;
+			if (!current->mm)
+				goto exit_put;
+			regs = task_pt_regs(current);
 		}
 
-		if (regs) {
-			if (add_mark)
-				perf_callchain_store_context(&ctx, PERF_CONTEXT_USER);
+		if (add_mark)
+			perf_callchain_store_context(&ctx, PERF_CONTEXT_USER);
 
-			start_entry_idx = entry->nr;
-			perf_callchain_user(&ctx, regs);
-			fixup_uretprobe_trampoline_entries(entry, start_entry_idx);
-		}
+		start_entry_idx = entry->nr;
+		perf_callchain_user(&ctx, regs);
+		fixup_uretprobe_trampoline_entries(entry, start_entry_idx);
 	}
 
+exit_put:
 	put_callchain_entry(rctx);
 
 	return entry;
Signature · 1 line
2.48.1
author maintainer
#150 lore raw

PATCH v4 34/39 perf: Skip user unwind if !current->mm

In reply to Josh Poimboeuf

If the task doesn't have any memory, there's no stack to unwind.

---
 kernel/events/core.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
kernel/events/core.c +1 -1
index 99f0f28feeb5..a886bb83f4d0 100644
@@ -7792,7 +7792,7 @@ struct perf_callchain_entry *
 perf_callchain(struct perf_event *event, struct pt_regs *regs)
 {
 	bool kernel = !event->attr.exclude_callchain_kernel;
-	bool user   = !event->attr.exclude_callchain_user;
+	bool user   = !event->attr.exclude_callchain_user && current->mm;
 	const u32 max_stack = event->attr.sample_max_stack;
 	struct perf_callchain_entry *callchain;
 
Signature · 1 line
2.48.1
maintainer
#151 lore raw

Re: [PATCH v4 34/39] perf: Skip user unwind if !current->mm

Quoted 12 lines · Josh Poimboeuf

If the task doesn't have any memory, there's no stack to unwind.

---
 kernel/events/core.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
kernel/events/core.c +0 -0
index 99f0f28feeb5..a886bb83f4d0 100644

kernel/events/core.c struct perf_callchain_entry * · line 7795

Quoted 5 lines
kernel/events/core.c +1 -1
@@ -7792,7 +7792,7 @@ struct perf_callchain_entry *
 perf_callchain(struct perf_event *event, struct pt_regs *regs)
 {
 	bool kernel = !event->attr.exclude_callchain_kernel;
-	bool user   = !event->attr.exclude_callchain_user;
+	bool user   = !event->attr.exclude_callchain_user && current->mm;

What about things like io_uring helpers, don't they keep ->mm but are never in userspace?

author maintainer
#152 lore raw

Re: [PATCH v4 34/39] perf: Skip user unwind if !current->mm

Quoted 8 lines · Peter Zijlstra
Quoted 7 lines · Josh Poimboeuf

If the task doesn't have any memory, there's no stack to unwind.

---
 kernel/events/core.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Quoted 13 lines

kernel/events/core.c struct perf_callchain_entry * · line 7795

Quoted 10 lines
kernel/events/core.c +1 -1
index 99f0f28feeb5..a886bb83f4d0 100644
@@ -7792,7 +7792,7 @@ struct perf_callchain_entry *
 perf_callchain(struct perf_event *event, struct pt_regs *regs)
 {
 	bool kernel = !event->attr.exclude_callchain_kernel;
-	bool user   = !event->attr.exclude_callchain_user;
+	bool user   = !event->attr.exclude_callchain_user && current->mm;

What about things like io_uring helpers, don't they keep ->mm but are never in userspace?

Hm, that's news to me. At least this patch doesn't make things worse in that regard.

IIRC, user_regs(task_pt_regs(current)) returns false for kthreads because the regs area is cleared. We could add in a user_regs() check?

Signature · 1 line
Josh
maintainer
#153 lore raw

Re: [PATCH v4 34/39] perf: Skip user unwind if !current->mm

Quoted 23 lines · Josh Poimboeuf
Quoted 8 lines · Peter Zijlstra
Quoted 7 lines · Josh Poimboeuf

If the task doesn't have any memory, there's no stack to unwind.

---
 kernel/events/core.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Quoted 13 lines

kernel/events/core.c struct perf_callchain_entry * · line 7795

Quoted 10 lines
kernel/events/core.c +1 -1
index 99f0f28feeb5..a886bb83f4d0 100644
@@ -7792,7 +7792,7 @@ struct perf_callchain_entry *
 perf_callchain(struct perf_event *event, struct pt_regs *regs)
 {
 	bool kernel = !event->attr.exclude_callchain_kernel;
-	bool user   = !event->attr.exclude_callchain_user;
+	bool user   = !event->attr.exclude_callchain_user && current->mm;

What about things like io_uring helpers, don't they keep ->mm but are never in userspace?

Quoted 5 lines

Hm, that's news to me. At least this patch doesn't make things worse in that regard.

IIRC, user_regs(task_pt_regs(current)) returns false for kthreads because the regs area is cleared. We could add in a user_regs() check?

Possibly, I've not yet dug into the io_worker thing in much detail yet.

author maintainer
#154 lore raw

PATCH v4 35/39 perf: Support deferred user callchains

In reply to Josh Poimboeuf

Use the new unwind_deferred_request() interface (if available) to defer unwinds to task context. This allows the use of .sframe (if available) and also prevents duplicate userspace unwinds.

---
 arch/Kconfig                          |   3 +
 include/linux/perf_event.h            |  13 +++-
 include/uapi/linux/perf_event.h       |  19 ++++-
 kernel/bpf/stackmap.c                 |   6 +-
 kernel/events/callchain.c             |  11 ++-
 kernel/events/core.c                  | 103 +++++++++++++++++++++++++-
 tools/include/uapi/linux/perf_event.h |  19 ++++-
 7 files changed, 166 insertions(+), 8 deletions(-)
arch/Kconfig +3 -0
index b3676605bab6..83ab94af46ca 100644
@@ -472,6 +472,9 @@ config SFRAME_VALIDATION
 
 	  If unsure, say N.
 
+config HAVE_PERF_CALLCHAIN_DEFERRED
+	bool
+
 config HAVE_PERF_REGS
 	bool
 	help
include/linux/perf_event.h +12 -1
index 1563dc2cd979..7fd54e4d2084 100644
@@ -62,6 +62,7 @@ struct perf_guest_info_callbacks {
 #include <linux/security.h>
 #include <linux/static_call.h>
 #include <linux/lockdep.h>
+#include <linux/unwind_deferred.h>
 #include <asm/local.h>
 
 struct perf_callchain_entry {
@@ -833,6 +834,10 @@ struct perf_event {
 	unsigned int			pending_work;
 	struct rcuwait			pending_work_wait;
 
+	struct unwind_work		pending_unwind_work;
+	struct rcuwait			pending_unwind_wait;
+	unsigned int			pending_unwind_callback;
+
 	atomic_t			event_limit;
 
 	/* address range filters */
@@ -1590,12 +1595,18 @@ extern void perf_callchain_user(struct perf_callchain_entry_ctx *entry, struct p
 extern void perf_callchain_kernel(struct perf_callchain_entry_ctx *entry, struct pt_regs *regs);
 extern struct perf_callchain_entry *
 get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
-		   u32 max_stack, bool add_mark);
+		   u32 max_stack, bool add_mark, bool defer_user);
 extern int get_callchain_buffers(int max_stack);
 extern void put_callchain_buffers(void);
 extern struct perf_callchain_entry *get_callchain_entry(int *rctx);
 extern void put_callchain_entry(int rctx);
 
+#ifdef CONFIG_HAVE_PERF_CALLCHAIN_DEFERRED
+extern void perf_callchain_user_deferred(struct perf_callchain_entry_ctx *entry, struct pt_regs *regs);
+#else
+static inline void perf_callchain_user_deferred(struct perf_callchain_entry_ctx *entry, struct pt_regs *regs) {}
+#endif
+
 extern int sysctl_perf_event_max_stack;
 extern int sysctl_perf_event_max_contexts_per_stack;
 
include/uapi/linux/perf_event.h +18 -1
index 0524d541d4e3..16307be57de9 100644
@@ -460,7 +460,8 @@ struct perf_event_attr {
 				inherit_thread :  1, /* children only inherit if cloned with CLONE_THREAD */
 				remove_on_exec :  1, /* event is removed from task on exec */
 				sigtrap        :  1, /* send synchronous SIGTRAP on event */
-				__reserved_1   : 26;
+				defer_callchain:  1, /* generate PERF_RECORD_CALLCHAIN_DEFERRED records */
+				__reserved_1   : 25;
 
 	union {
 		__u32		wakeup_events;	  /* wakeup every n events */
@@ -1226,6 +1227,21 @@ enum perf_event_type {
 	 */
 	PERF_RECORD_AUX_OUTPUT_HW_ID		= 21,
 
+	/*
+	 * This user callchain capture was deferred until shortly before
+	 * returning to user space.  Previous samples would have kernel
+	 * callchains only and they need to be stitched with this to make full
+	 * callchains.
+	 *
+	 * struct {
+	 *	struct perf_event_header	header;
+	 *	u64				nr;
+	 *	u64				ips[nr];
+	 *	struct sample_id		sample_id;
+	 * };
+	 */
+	PERF_RECORD_CALLCHAIN_DEFERRED		= 22,
+
 	PERF_RECORD_MAX,			/* non-ABI */
 };
 
@@ -1256,6 +1272,7 @@ enum perf_callchain_context {
 	PERF_CONTEXT_HV			= (__u64)-32,
 	PERF_CONTEXT_KERNEL		= (__u64)-128,
 	PERF_CONTEXT_USER		= (__u64)-512,
+	PERF_CONTEXT_USER_DEFERRED	= (__u64)-640,
 
 	PERF_CONTEXT_GUEST		= (__u64)-2048,
 	PERF_CONTEXT_GUEST_KERNEL	= (__u64)-2176,
kernel/bpf/stackmap.c +3 -3
index ee9701337912..f073ebaf9c30 100644
@@ -314,8 +314,7 @@ BPF_CALL_3(bpf_get_stackid, struct pt_regs *, regs, struct bpf_map *, map,
 	if (max_depth > sysctl_perf_event_max_stack)
 		max_depth = sysctl_perf_event_max_stack;
 
-	trace = get_perf_callchain(regs, kernel, user, max_depth, false);
-
+	trace = get_perf_callchain(regs, kernel, user, max_depth, false, false);
 	if (unlikely(!trace))
 		/* couldn't fetch the stack trace */
 		return -EFAULT;
@@ -448,7 +447,8 @@ static long __bpf_get_stack(struct pt_regs *regs, struct task_struct *task,
 	else if (kernel && task)
 		trace = get_callchain_entry_for_task(task, max_depth);
 	else
-		trace = get_perf_callchain(regs, kernel, user, max_depth,false);
+		trace = get_perf_callchain(regs, kernel, user, max_depth,
+					   false, false);
 
 	if (unlikely(!trace) || trace->nr < skip) {
 		if (may_fault)
kernel/events/callchain.c +10 -1
index 2278402b7ac9..eeb15ba0137f 100644
@@ -217,7 +217,7 @@ static void fixup_uretprobe_trampoline_entries(struct perf_callchain_entry *entr
 
 struct perf_callchain_entry *
 get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
-		   u32 max_stack, bool add_mark)
+		   u32 max_stack, bool add_mark, bool defer_user)
 {
 	struct perf_callchain_entry *entry;
 	struct perf_callchain_entry_ctx ctx;
@@ -246,6 +246,15 @@ get_perf_callchain(struct pt_regs *regs, bool kernel, bool user,
 			regs = task_pt_regs(current);
 		}
 
+		if (defer_user) {
+			/*
+			 * Foretell the coming of PERF_RECORD_CALLCHAIN_DEFERRED
+			 * which can be stitched to this one.
+			 */
+			perf_callchain_store_context(&ctx, PERF_CONTEXT_USER_DEFERRED);
+			goto exit_put;
+		}
+
 		if (add_mark)
 			perf_callchain_store_context(&ctx, PERF_CONTEXT_USER);
 
kernel/events/core.c +102 -1

144 lines not on this page · raw message

tools/include/uapi/linux/perf_event.h +18 -1
index 0524d541d4e3..16307be57de9 100644
@@ -460,7 +460,8 @@ struct perf_event_attr {
 				inherit_thread :  1, /* children only inherit if cloned with CLONE_THREAD */
 				remove_on_exec :  1, /* event is removed from task on exec */
 				sigtrap        :  1, /* send synchronous SIGTRAP on event */
-				__reserved_1   : 26;
+				defer_callchain:  1, /* generate PERF_RECORD_CALLCHAIN_DEFERRED records */
+				__reserved_1   : 25;
 
 	union {
 		__u32		wakeup_events;	  /* wakeup every n events */
@@ -1226,6 +1227,21 @@ enum perf_event_type {
 	 */
 	PERF_RECORD_AUX_OUTPUT_HW_ID		= 21,
 
+	/*
+	 * This user callchain capture was deferred until shortly before
+	 * returning to user space.  Previous samples would have kernel
+	 * callchains only and they need to be stitched with this to make full
+	 * callchains.
+	 *
+	 * struct {
+	 *	struct perf_event_header	header;
+	 *	u64				nr;
+	 *	u64				ips[nr];
+	 *	struct sample_id		sample_id;
+	 * };
+	 */
+	PERF_RECORD_CALLCHAIN_DEFERRED		= 22,
+
 	PERF_RECORD_MAX,			/* non-ABI */
 };
 
@@ -1256,6 +1272,7 @@ enum perf_callchain_context {
 	PERF_CONTEXT_HV			= (__u64)-32,
 	PERF_CONTEXT_KERNEL		= (__u64)-128,
 	PERF_CONTEXT_USER		= (__u64)-512,
+	PERF_CONTEXT_USER_DEFERRED	= (__u64)-640,
 
 	PERF_CONTEXT_GUEST		= (__u64)-2048,
 	PERF_CONTEXT_GUEST_KERNEL	= (__u64)-2176,
Signature · 1 line
2.48.1
author maintainer
#155 lore raw

PATCH v4 36/39 perf tools: Minimal CALLCHAIN_DEFERRED support

In reply to Josh Poimboeuf

From: Namhyung Kim <[email protected]>

Add a new event type for deferred callchains and a new callback for the struct perf_tool. For now it doesn't actually handle the deferred callchains but it just marks the sample if it has the PERF_CONTEXT_ USER_DEFFERED in the callchain array.

At least, perf report can dump the raw data with this change. Actually this requires the next commit to enable attr.defer_callchain, but if you already have a data file, it'll show the following result.

  $ perf report -D
  ...
  [email protected] [0x40]: event: 22
  .
  . ... raw event: size 64 bytes
  .  0000:  16 00 00 00 02 00 40 00 02 00 00 00 00 00 00 00  ......@.........
  .  0010:  00 fe ff ff ff ff ff ff 4b d3 3f 25 45 7f 00 00  ........K.?%E...
  .  0020:  21 03 00 00 21 03 00 00 43 02 12 ab 05 00 00 00  !...!...C.......
  .  0030:  00 00 00 00 00 00 00 00 09 00 00 00 00 00 00 00  ................

0 24344920643 0x5fe0 [0x40]: PERF_RECORD_CALLCHAIN_DEFERRED(IP, 0x2): 801/801: 0 ... FP chain: nr:2
..... 0: fffffffffffffe00
..... 1: 00007f45253fd34b
: unhandled!

---
 tools/lib/perf/include/perf/event.h       |  7 +++++++
 tools/perf/util/event.c                   |  1 +
 tools/perf/util/evsel.c                   | 15 +++++++++++++++
 tools/perf/util/machine.c                 |  1 +
 tools/perf/util/perf_event_attr_fprintf.c |  1 +
 tools/perf/util/sample.h                  |  3 ++-
 tools/perf/util/session.c                 | 17 +++++++++++++++++
 tools/perf/util/tool.c                    |  1 +
 tools/perf/util/tool.h                    |  3 ++-
 9 files changed, 47 insertions(+), 2 deletions(-)
tools/lib/perf/include/perf/event.h +7 -0
index 37bb7771d914..f643a6a2b9fc 100644
@@ -151,6 +151,12 @@ struct perf_record_switch {
 	__u32			 next_prev_tid;
 };
 
+struct perf_record_callchain_deferred {
+	struct perf_event_header header;
+	__u64			 nr;
+	__u64			 ips[];
+};
+
 struct perf_record_header_attr {
 	struct perf_event_header header;
 	struct perf_event_attr	 attr;
@@ -494,6 +500,7 @@ union perf_event {
 	struct perf_record_read			read;
 	struct perf_record_throttle		throttle;
 	struct perf_record_sample		sample;
+	struct perf_record_callchain_deferred	callchain_deferred;
 	struct perf_record_bpf_event		bpf;
 	struct perf_record_ksymbol		ksymbol;
 	struct perf_record_text_poke_event	text_poke;
tools/perf/util/event.c +1 -0
index aac96d5d1917..8cdec373db44 100644
@@ -58,6 +58,7 @@ static const char *perf_event__names[] = {
 	[PERF_RECORD_CGROUP]			= "CGROUP",
 	[PERF_RECORD_TEXT_POKE]			= "TEXT_POKE",
 	[PERF_RECORD_AUX_OUTPUT_HW_ID]		= "AUX_OUTPUT_HW_ID",
+	[PERF_RECORD_CALLCHAIN_DEFERRED]	= "CALLCHAIN_DEFERRED",
 	[PERF_RECORD_HEADER_ATTR]		= "ATTR",
 	[PERF_RECORD_HEADER_EVENT_TYPE]		= "EVENT_TYPE",
 	[PERF_RECORD_HEADER_TRACING_DATA]	= "TRACING_DATA",
tools/perf/util/evsel.c +15 -0
index d22c5df1701e..09b9735f2fb1 100644
@@ -2817,6 +2817,18 @@ int evsel__parse_sample(struct evsel *evsel, union perf_event *event,
 	data->data_src = PERF_MEM_DATA_SRC_NONE;
 	data->vcpu = -1;
 
+	if (event->header.type == PERF_RECORD_CALLCHAIN_DEFERRED) {
+		const u64 max_callchain_nr = UINT64_MAX / sizeof(u64);
+
+		data->callchain = (struct ip_callchain *)&event->callchain_deferred.nr;
+		if (data->callchain->nr > max_callchain_nr)
+			return -EFAULT;
+
+		if (evsel->core.attr.sample_id_all)
+			perf_evsel__parse_id_sample(evsel, event, data);
+		return 0;
+	}
+
 	if (event->header.type != PERF_RECORD_SAMPLE) {
 		if (!evsel->core.attr.sample_id_all)
 			return 0;
@@ -2947,6 +2959,9 @@ int evsel__parse_sample(struct evsel *evsel, union perf_event *event,
 		if (data->callchain->nr > max_callchain_nr)
 			return -EFAULT;
 		sz = data->callchain->nr * sizeof(u64);
+		if (evsel->core.attr.defer_callchain && data->callchain->nr >= 1 &&
+		    data->callchain->ips[data->callchain->nr - 1] == PERF_CONTEXT_USER_DEFERRED)
+			data->deferred_callchain = true;
 		OVERFLOW_CHECK(array, sz, max_size);
 		array = (void *)array + sz;
 	}
tools/perf/util/machine.c +1 -0
index 27d5345d2b30..9da467886bc6 100644
@@ -2087,6 +2087,7 @@ static int add_callchain_ip(struct thread *thread,
 				*cpumode = PERF_RECORD_MISC_KERNEL;
 				break;
 			case PERF_CONTEXT_USER:
+			case PERF_CONTEXT_USER_DEFERRED:
 				*cpumode = PERF_RECORD_MISC_USER;
 				break;
 			default:
tools/perf/util/perf_event_attr_fprintf.c +1 -0
index 59fbbba79697..113845b35110 100644
@@ -321,6 +321,7 @@ int perf_event_attr__fprintf(FILE *fp, struct perf_event_attr *attr,
 	PRINT_ATTRf(inherit_thread, p_unsigned);
 	PRINT_ATTRf(remove_on_exec, p_unsigned);
 	PRINT_ATTRf(sigtrap, p_unsigned);
+	PRINT_ATTRf(defer_callchain, p_unsigned);
 
 	PRINT_ATTRn("{ wakeup_events, wakeup_watermark }", wakeup_events, p_unsigned, false);
 	PRINT_ATTRf(bp_type, p_unsigned);
tools/perf/util/sample.h +2 -1
index 70b2c3135555..010659dc80f8 100644
@@ -108,7 +108,8 @@ struct perf_sample {
 		u16 p_stage_cyc;
 		u16 retire_lat;
 	};
-	bool no_hw_idx;		/* No hw_idx collected in branch_stack */
+	bool no_hw_idx;			/* No hw_idx collected in branch_stack */
+	bool deferred_callchain;	/* Has deferred user callchains */
 	char insn[MAX_INSN];
 	void *raw_data;
 	struct ip_callchain *callchain;
tools/perf/util/session.c +17 -0
index 507e6cba9545..493070180279 100644
@@ -714,6 +714,7 @@ static perf_event__swap_op perf_event__swap_ops[] = {
 	[PERF_RECORD_CGROUP]		  = perf_event__cgroup_swap,
 	[PERF_RECORD_TEXT_POKE]		  = perf_event__text_poke_swap,
 	[PERF_RECORD_AUX_OUTPUT_HW_ID]	  = perf_event__all64_swap,
+	[PERF_RECORD_CALLCHAIN_DEFERRED]  = perf_event__all64_swap,
 	[PERF_RECORD_HEADER_ATTR]	  = perf_event__hdr_attr_swap,
 	[PERF_RECORD_HEADER_EVENT_TYPE]	  = perf_event__event_type_swap,
 	[PERF_RECORD_HEADER_TRACING_DATA] = perf_event__tracing_data_swap,
@@ -1107,6 +1108,19 @@ static void dump_sample(struct evsel *evsel, union perf_event *event,
 		sample_read__printf(sample, evsel->core.attr.read_format);
 }
 
+static void dump_deferred_callchain(struct evsel *evsel, union perf_event *event,
+				    struct perf_sample *sample)
+{
+	if (!dump_trace)
+		return;
+
+	printf("(IP, 0x%x): %d/%d: %#" PRIx64 "\n",
+	       event->header.misc, sample->pid, sample->tid, sample->ip);
+
+	if (evsel__has_callchain(evsel))
+		callchain__printf(evsel, sample);
+}
+
 static void dump_read(struct evsel *evsel, union perf_event *event)
 {
 	struct perf_record_read *read_event = &event->read;
@@ -1337,6 +1351,9 @@ static int machines__deliver_event(struct machines *machines,
 		return tool->text_poke(tool, event, sample, machine);
 	case PERF_RECORD_AUX_OUTPUT_HW_ID:
 		return tool->aux_output_hw_id(tool, event, sample, machine);
+	case PERF_RECORD_CALLCHAIN_DEFERRED:
+		dump_deferred_callchain(evsel, event, sample);
+		return tool->callchain_deferred(tool, event, sample, evsel, machine);
 	default:
 		++evlist->stats.nr_unknown_events;
 		return -1;
tools/perf/util/tool.c +1 -0
index 3b7f390f26eb..e78f16de912e 100644
@@ -259,6 +259,7 @@ void perf_tool__init(struct perf_tool *tool, bool ordered_events)
 	tool->read = process_event_sample_stub;
 	tool->throttle = process_event_stub;
 	tool->unthrottle = process_event_stub;
+	tool->callchain_deferred = process_event_sample_stub;
 	tool->attr = process_event_synth_attr_stub;
 	tool->event_update = process_event_synth_event_update_stub;
 	tool->tracing_data = process_event_synth_tracing_data_stub;
tools/perf/util/tool.h +2 -1
index db1c7642b0d1..9987bbde6d5e 100644
@@ -42,7 +42,8 @@ enum show_feature_header {
 
 struct perf_tool {
 	event_sample	sample,
-			read;
+			read,
+			callchain_deferred;
 	event_op	mmap,
 			mmap2,
 			comm,
Signature · 1 line
2.48.1
author maintainer
#156 lore raw

PATCH v4 37/39 perf record: Enable defer_callchain for user callchains

In reply to Josh Poimboeuf

From: Namhyung Kim <[email protected]>

And add the missing feature detection logic to clear the flag on old kernels.

  $ perf record -g -vv true
  ...
  ------------------------------------------------------------
  perf_event_attr:
    type                             0 (PERF_TYPE_HARDWARE)
    size                             136
    config                           0 (PERF_COUNT_HW_CPU_CYCLES)
    { sample_period, sample_freq }   4000
    sample_type                      IP|TID|TIME|CALLCHAIN|PERIOD
    read_format                      ID|LOST
    disabled                         1
    inherit                          1
    mmap                             1
    comm                             1
    freq                             1
    enable_on_exec                   1
    task                             1
    sample_id_all                    1
    mmap2                            1
    comm_exec                        1
    ksymbol                          1
    bpf_event                        1
    defer_callchain                  1
  ------------------------------------------------------------
  sys_perf_event_open: pid 162755  cpu 0  group_fd -1  flags 0x8
  sys_perf_event_open failed, error -22
  switching off deferred callchain support
---
 tools/perf/util/evsel.c | 24 ++++++++++++++++++++++++
 tools/perf/util/evsel.h |  1 +
 2 files changed, 25 insertions(+)
tools/perf/util/evsel.c +24 -0
index 09b9735f2fb1..e3be3cc7632d 100644
@@ -998,6 +998,14 @@ static void __evsel__config_callchain(struct evsel *evsel, struct record_opts *o
 		}
 	}
 
+	if (param->record_mode == CALLCHAIN_FP && !attr->exclude_callchain_user) {
+		/*
+		 * Enable deferred callchains optimistically.  It'll be switched
+		 * off later if the kernel doesn't support it.
+		 */
+		attr->defer_callchain = 1;
+	}
+
 	if (function) {
 		pr_info("Disabling user space callchains for function trace event.\n");
 		attr->exclude_callchain_user = 1;
@@ -2038,6 +2046,8 @@ static int __evsel__prepare_open(struct evsel *evsel, struct perf_cpu_map *cpus,
 
 static void evsel__disable_missing_features(struct evsel *evsel)
 {
+	if (perf_missing_features.defer_callchain)
+		evsel->core.attr.defer_callchain = 0;
 	if (perf_missing_features.inherit_sample_read && evsel->core.attr.inherit &&
 	    (evsel->core.attr.sample_type & PERF_SAMPLE_READ))
 		evsel->core.attr.inherit = 0;
@@ -2244,6 +2254,15 @@ static bool evsel__detect_missing_features(struct evsel *evsel)
 
 	/* Please add new feature detection here. */
 
+	attr.defer_callchain = true;
+	attr.sample_type = PERF_SAMPLE_CALLCHAIN;
+	if (has_attr_feature(&attr, /*flags=*/0))
+		goto found;
+	perf_missing_features.defer_callchain = true;
+	pr_debug2("switching off deferred callchain support\n");
+	attr.defer_callchain = false;
+	attr.sample_type = 0;
+
 	attr.inherit = true;
 	attr.sample_type = PERF_SAMPLE_READ;
 	if (has_attr_feature(&attr, /*flags=*/0))
@@ -2355,6 +2374,11 @@ static bool evsel__detect_missing_features(struct evsel *evsel)
 	errno = old_errno;
 
 check:
+	if (evsel->core.attr.defer_callchain &&
+	    evsel->core.attr.sample_type & PERF_SAMPLE_CALLCHAIN &&
+	    perf_missing_features.defer_callchain)
+		return true;
+
 	if (evsel->core.attr.inherit &&
 	    (evsel->core.attr.sample_type & PERF_SAMPLE_READ) &&
 	    perf_missing_features.inherit_sample_read)
tools/perf/util/evsel.h +1 -0
index 04934a7af174..b90a970f9a30 100644
@@ -206,6 +206,7 @@ struct perf_missing_features {
 	bool read_lost;
 	bool branch_counters;
 	bool inherit_sample_read;
+	bool defer_callchain;
 };
 
 extern struct perf_missing_features perf_missing_features;
Signature · 1 line
2.48.1
author maintainer
#157 lore raw

PATCH v4 38/39 perf script: Display PERF_RECORD_CALLCHAIN_DEFERRED

In reply to Josh Poimboeuf

From: Namhyung Kim <[email protected]>

Handle the deferred callchains in the script output.

  $ perf script
  perf     801 [000]    18.031793:          1 cycles:P:
          ffffffff91a14c36 __intel_pmu_enable_all.isra.0+0x56 ([kernel.kallsyms])
          ffffffff91d373e9 perf_ctx_enable+0x39 ([kernel.kallsyms])
          ffffffff91d36af7 event_function+0xd7 ([kernel.kallsyms])
          ffffffff91d34222 remote_function+0x42 ([kernel.kallsyms])
          ffffffff91c1ebe1 generic_exec_single+0x61 ([kernel.kallsyms])
          ffffffff91c1edac smp_call_function_single+0xec ([kernel.kallsyms])
          ffffffff91d37a9d event_function_call+0x10d ([kernel.kallsyms])
          ffffffff91d33557 perf_event_for_each_child+0x37 ([kernel.kallsyms])
          ffffffff91d47324 _perf_ioctl+0x204 ([kernel.kallsyms])
          ffffffff91d47c43 perf_ioctl+0x33 ([kernel.kallsyms])
          ffffffff91e2f216 __x64_sys_ioctl+0x96 ([kernel.kallsyms])
          ffffffff9265f1ae do_syscall_64+0x9e ([kernel.kallsyms])
          ffffffff92800130 entry_SYSCALL_64+0xb0 ([kernel.kallsyms])

perf 801 [000] 18.031814: DEFERRED CALLCHAIN

              7fb5fc22034b __GI___ioctl+0x3b (/usr/lib/x86_64-linux-gnu/libc.so.6)
---
 tools/perf/builtin-script.c | 89 +++++++++++++++++++++++++++++++++++++
 1 file changed, 89 insertions(+)
tools/perf/builtin-script.c +89 -0
index 9e47905f75a6..2b9085fa18bd 100644
@@ -2541,6 +2541,93 @@ static int process_sample_event(const struct perf_tool *tool,
 	return ret;
 }
 
+static int process_deferred_sample_event(const struct perf_tool *tool,
+					 union perf_event *event,
+					 struct perf_sample *sample,
+					 struct evsel *evsel,
+					 struct machine *machine)
+{
+	struct perf_script *scr = container_of(tool, struct perf_script, tool);
+	struct perf_event_attr *attr = &evsel->core.attr;
+	struct evsel_script *es = evsel->priv;
+	unsigned int type = output_type(attr->type);
+	struct addr_location al;
+	FILE *fp = es->fp;
+	int ret = 0;
+
+	if (output[type].fields == 0)
+		return 0;
+
+	/* Set thread to NULL to indicate addr_al and al are not initialized */
+	addr_location__init(&al);
+
+	if (perf_time__ranges_skip_sample(scr->ptime_range, scr->range_num,
+					  sample->time)) {
+		goto out_put;
+	}
+
+	if (debug_mode) {
+		if (sample->time < last_timestamp) {
+			pr_err("Samples misordered, previous: %" PRIu64
+				" this: %" PRIu64 "\n", last_timestamp,
+				sample->time);
+			nr_unordered++;
+		}
+		last_timestamp = sample->time;
+		goto out_put;
+	}
+
+	if (filter_cpu(sample))
+		goto out_put;
+
+	if (machine__resolve(machine, &al, sample) < 0) {
+		pr_err("problem processing %d event, skipping it.\n",
+		       event->header.type);
+		ret = -1;
+		goto out_put;
+	}
+
+	if (al.filtered)
+		goto out_put;
+
+	if (!show_event(sample, evsel, al.thread, &al, NULL))
+		goto out_put;
+
+	if (evswitch__discard(&scr->evswitch, evsel))
+		goto out_put;
+
+	perf_sample__fprintf_start(scr, sample, al.thread, evsel,
+				   PERF_RECORD_CALLCHAIN_DEFERRED, fp);
+	fprintf(fp, "DEFERRED CALLCHAIN");
+
+	if (PRINT_FIELD(IP)) {
+		struct callchain_cursor *cursor = NULL;
+
+		if (symbol_conf.use_callchain && sample->callchain) {
+			cursor = get_tls_callchain_cursor();
+			if (thread__resolve_callchain(al.thread, cursor, evsel,
+						      sample, NULL, NULL,
+						      scripting_max_stack)) {
+				pr_info("cannot resolve deferred callchains\n");
+				cursor = NULL;
+			}
+		}
+
+		fputc(cursor ? '\n' : ' ', fp);
+		sample__fprintf_sym(sample, &al, 0, output[type].print_ip_opts,
+				    cursor, symbol_conf.bt_stop_list, fp);
+	}
+
+	fprintf(fp, "\n");
+
+	if (verbose > 0)
+		fflush(fp);
+
+out_put:
+	addr_location__exit(&al);
+	return ret;
+}
+
 // Used when scr->per_event_dump is not set
 static struct evsel_script es_stdout;
 
@@ -4326,6 +4413,7 @@ int cmd_script(int argc, const char **argv)
 
 	perf_tool__init(&script.tool, !unsorted_dump);
 	script.tool.sample		 = process_sample_event;
+	script.tool.callchain_deferred	 = process_deferred_sample_event;
 	script.tool.mmap		 = perf_event__process_mmap;
 	script.tool.mmap2		 = perf_event__process_mmap2;
 	script.tool.comm		 = perf_event__process_comm;
@@ -4352,6 +4440,7 @@ int cmd_script(int argc, const char **argv)
 	script.tool.throttle		 = process_throttle_event;
 	script.tool.unthrottle		 = process_throttle_event;
 	script.tool.ordering_requires_timestamps = true;
+	script.tool.merge_deferred_callchains = false;
 	session = perf_session__new(&data, &script.tool);
 	if (IS_ERR(session))
 		return PTR_ERR(session);
Signature · 1 line
2.48.1
author maintainer
#158 lore raw

PATCH v4 39/39 perf tools: Merge deferred user callchains

In reply to Josh Poimboeuf

From: Namhyung Kim <[email protected]>

Save samples with deferred callchains in a separate list and deliver them after merging the user callchains. If users don't want to merge they can set tool->merge_deferred_callchains to false to prevent the behavior.

With previous result, now perf script will show the merged callchains.

  $ perf script
  perf     801 [000]    18.031793:          1 cycles:P:
          ffffffff91a14c36 __intel_pmu_enable_all.isra.0+0x56 ([kernel.kallsyms])
          ffffffff91d373e9 perf_ctx_enable+0x39 ([kernel.kallsyms])
          ffffffff91d36af7 event_function+0xd7 ([kernel.kallsyms])
          ffffffff91d34222 remote_function+0x42 ([kernel.kallsyms])
          ffffffff91c1ebe1 generic_exec_single+0x61 ([kernel.kallsyms])
          ffffffff91c1edac smp_call_function_single+0xec ([kernel.kallsyms])
          ffffffff91d37a9d event_function_call+0x10d ([kernel.kallsyms])
          ffffffff91d33557 perf_event_for_each_child+0x37 ([kernel.kallsyms])
          ffffffff91d47324 _perf_ioctl+0x204 ([kernel.kallsyms])
          ffffffff91d47c43 perf_ioctl+0x33 ([kernel.kallsyms])
          ffffffff91e2f216 __x64_sys_ioctl+0x96 ([kernel.kallsyms])
          ffffffff9265f1ae do_syscall_64+0x9e ([kernel.kallsyms])
          ffffffff92800130 entry_SYSCALL_64+0xb0 ([kernel.kallsyms])
              7fb5fc22034b __GI___ioctl+0x3b (/usr/lib/x86_64-linux-gnu/libc.so.6)
  ...

The old output can be get using --no-merge-callchain option. Also perf report can get the user callchain entry at the end.

  $ perf report --no-children --percent-limit=0 --stdio -q -S __intel_pmu_enable_all.isra.0
  # symbol: __intel_pmu_enable_all.isra.0
       0.00%  perf     [kernel.kallsyms]
              |
              ---__intel_pmu_enable_all.isra.0
                 perf_ctx_enable
                 event_function
                 remote_function
                 generic_exec_single
                 smp_call_function_single
                 event_function_call
                 perf_event_for_each_child
                 _perf_ioctl
                 perf_ioctl
                 __x64_sys_ioctl
                 do_syscall_64
                 entry_SYSCALL_64
                 __GI___ioctl
---
 tools/perf/Documentation/perf-script.txt |  5 ++
 tools/perf/builtin-script.c              |  5 +-
 tools/perf/util/callchain.c              | 24 +++++++++
 tools/perf/util/callchain.h              |  3 ++
 tools/perf/util/evlist.c                 |  1 +
 tools/perf/util/evlist.h                 |  1 +
 tools/perf/util/session.c                | 63 +++++++++++++++++++++++-
 tools/perf/util/tool.c                   |  1 +
 tools/perf/util/tool.h                   |  1 +
 9 files changed, 102 insertions(+), 2 deletions(-)
tools/perf/Documentation/perf-script.txt +5 -0
index b72866ef270b..69f018b3d199 100644
@@ -518,6 +518,11 @@ include::itrace.txt[]
 	The known limitations include exception handing such as
 	setjmp/longjmp will have calls/returns not match.
 
+--merge-callchains::
+	Enable merging deferred user callchains if available.  This is the
+	default behavior.  If you want to see separate CALLCHAIN_DEFERRED
+	records for some reason, use --no-merge-callchains explicitly.
+
 :GMEXAMPLECMD: script
 :GMEXAMPLESUBCMD:
 include::guest-files.txt[]
tools/perf/builtin-script.c +4 -1
index 2b9085fa18bd..d18ada14a83a 100644
@@ -4032,6 +4032,7 @@ int cmd_script(int argc, const char **argv)
 	bool header_only = false;
 	bool script_started = false;
 	bool unsorted_dump = false;
+	bool merge_deferred_callchains = true;
 	char *rec_script_path = NULL;
 	char *rep_script_path = NULL;
 	struct perf_session *session;
@@ -4185,6 +4186,8 @@ int cmd_script(int argc, const char **argv)
 		    "Guest code can be found in hypervisor process"),
 	OPT_BOOLEAN('\0', "stitch-lbr", &script.stitch_lbr,
 		    "Enable LBR callgraph stitching approach"),
+	OPT_BOOLEAN('\0', "merge-callchains", &merge_deferred_callchains,
+		    "Enable merge deferred user callchains"),
 	OPTS_EVSWITCH(&script.evswitch),
 	OPT_END()
 	};
@@ -4440,7 +4443,7 @@ int cmd_script(int argc, const char **argv)
 	script.tool.throttle		 = process_throttle_event;
 	script.tool.unthrottle		 = process_throttle_event;
 	script.tool.ordering_requires_timestamps = true;
-	script.tool.merge_deferred_callchains = false;
+	script.tool.merge_deferred_callchains = merge_deferred_callchains;
 	session = perf_session__new(&data, &script.tool);
 	if (IS_ERR(session))
 		return PTR_ERR(session);
tools/perf/util/callchain.c +24 -0
index 0c7564747a14..d1114491c3da 100644
@@ -1832,3 +1832,27 @@ int sample__for_each_callchain_node(struct thread *thread, struct evsel *evsel,
 	}
 	return 0;
 }
+
+int sample__merge_deferred_callchain(struct perf_sample *sample_orig,
+				     struct perf_sample *sample_callchain)
+{
+	u64 nr_orig = sample_orig->callchain->nr - 1;
+	u64 nr_deferred = sample_callchain->callchain->nr;
+	struct ip_callchain *callchain;
+
+	callchain = calloc(1 + nr_orig + nr_deferred, sizeof(u64));
+	if (callchain == NULL) {
+		sample_orig->deferred_callchain = false;
+		return -ENOMEM;
+	}
+
+	callchain->nr = nr_orig + nr_deferred;
+	/* copy except for the last PERF_CONTEXT_USER_DEFERRED */
+	memcpy(callchain->ips, sample_orig->callchain->ips, nr_orig * sizeof(u64));
+	/* copy deferred use callchains */
+	memcpy(&callchain->ips[nr_orig], sample_callchain->callchain->ips,
+	       nr_deferred * sizeof(u64));
+
+	sample_orig->callchain = callchain;
+	return 0;
+}
tools/perf/util/callchain.h +3 -0
index 86ed9e4d04f9..89785125ed25 100644
@@ -317,4 +317,7 @@ int sample__for_each_callchain_node(struct thread *thread, struct evsel *evsel,
 				    struct perf_sample *sample, int max_stack,
 				    bool symbols, callchain_iter_fn cb, void *data);
 
+int sample__merge_deferred_callchain(struct perf_sample *sample_orig,
+				     struct perf_sample *sample_callchain);
+
 #endif	/* __PERF_CALLCHAIN_H */
tools/perf/util/evlist.c +1 -0
index f0dd174e2deb..39a43980f6aa 100644
@@ -82,6 +82,7 @@ void evlist__init(struct evlist *evlist, struct perf_cpu_map *cpus,
 	evlist->ctl_fd.ack = -1;
 	evlist->ctl_fd.pos = -1;
 	evlist->nr_br_cntr = -1;
+	INIT_LIST_HEAD(&evlist->deferred_samples);
 }
 
 struct evlist *evlist__new(void)
tools/perf/util/evlist.h +1 -0
index adddb1db1ad2..f78275af1553 100644
@@ -84,6 +84,7 @@ struct evlist {
 		int	pos;	/* index at evlist core object to check signals */
 	} ctl_fd;
 	struct event_enable_timer *eet;
+	struct list_head deferred_samples;
 };
 
 struct evsel_str_handler {
tools/perf/util/session.c +62 -1

81 lines not on this page · raw message

tools/perf/util/tool.c +1 -0
index e78f16de912e..385043e06627 100644
@@ -238,6 +238,7 @@ void perf_tool__init(struct perf_tool *tool, bool ordered_events)
 	tool->cgroup_events = false;
 	tool->no_warn = false;
 	tool->show_feat_hdr = SHOW_FEAT_NO_HEADER;
+	tool->merge_deferred_callchains = true;
 
 	tool->sample = process_event_sample_stub;
 	tool->mmap = process_event_stub;
tools/perf/util/tool.h +1 -0
index 9987bbde6d5e..d06580478ab1 100644
@@ -87,6 +87,7 @@ struct perf_tool {
 	bool		cgroup_events;
 	bool		no_warn;
 	bool		dont_split_sample_group;
+	bool		merge_deferred_callchains;
 	enum show_feature_header show_feat_hdr;
 };
 
Signature · 1 line
2.48.1
author maintainer
#159 lore raw
In reply to Josh Poimboeuf
Quoted 7 lines · Josh Poimboeuf

For testing with user space, here are the latest binutils fixes:

1785837a2570 ("ld: fix PR/32297")
938fb512184d ("ld: fix wrong SFrame info for lazy IBT PLT") 47c88752f9ad ("ld: generate SFrame stack trace info for .plt.got")

An out-of-tree glibc patch is also needed -- will attach in a reply.

Latest out-of-tree glibc patch below:

elf/dl-load.c +29 -0
index e986d7faab..5a593c2126 100644
@@ -29,6 +29,7 @@
 #include <bits/wordsize.h>
 #include <sys/mman.h>
 #include <sys/param.h>
+#include <sys/prctl.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <gnu/lib-names.h>
@@ -87,6 +88,9 @@ struct filebuf
 
 #define STRING(x) __STRING (x)
 
+#ifndef PT_GNU_SFRAME
+#define PT_GNU_SFRAME 0x6474e554
+#endif
 
 /* This is the decomposed LD_LIBRARY_PATH search path.  */
 struct r_search_path_struct __rtld_env_path_list attribute_relro;
@@ -1186,6 +1190,11 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
 	  l->l_relro_addr = ph->p_vaddr;
 	  l->l_relro_size = ph->p_memsz;
 	  break;
+
+	case PT_GNU_SFRAME:
+	  l->l_sframe_start = ph->p_vaddr;
+	  l->l_sframe_end   = ph->p_vaddr + ph->p_memsz;
+	  break;
 	}
 
     if (__glibc_unlikely (nloadcmds == 0))
@@ -1236,6 +1245,26 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
 	l->l_map_start = l->l_map_end = 0;
 	goto lose;
       }
+
+#define PR_ADD_SFRAME 77
+    if (l->l_sframe_start != 0)
+    {
+      l->l_sframe_start += l->l_addr;
+      l->l_sframe_end   += l->l_addr;
+
+      for (size_t i = 0; i < nloadcmds; i++)
+      {
+	struct loadcmd *c = &loadcmds[i];
+
+	if (c->prot & PROT_EXEC)
+	{
+	  ElfW(Addr) text_start = l->l_addr + c->mapstart;
+	  ElfW(Addr) text_end   = l->l_addr + c->mapend;
+
+	  __prctl(PR_ADD_SFRAME, l->l_sframe_start, l->l_sframe_end, text_start, text_end);
+	}
+      }
+    }
   }
 
   if (l->l_ld != NULL)
elf/dl-unmap-segments.h +6 -0
index f16f4d7ded..dd14162e00 100644
@@ -21,14 +21,20 @@
 
 #include <link.h>
 #include <sys/mman.h>
+#include <sys/prctl.h>
 
 /* _dl_map_segments ensures that any whole pages in gaps between segments
    are filled in with PROT_NONE mappings.  So we can just unmap the whole
    range in one fell swoop.  */
 
+#define PR_REMOVE_SFRAME 78
+
 static __always_inline void
 _dl_unmap_segments (struct link_map *l)
 {
+  if (l->l_sframe_start != 0)
+    __prctl(PR_REMOVE_SFRAME, l->l_sframe_start, NULL, NULL, NULL);
+
   __munmap ((void *) l->l_map_start, l->l_map_end - l->l_map_start);
 }
 
elf/setup-vdso.h +22 -0
index 888e1e4897..2a6bb9b944 100644
@@ -16,6 +16,11 @@
    License along with the GNU C Library; if not, see
    <https://www.gnu.org/licenses/>.  */
 
+#include <sys/prctl.h>
+#ifndef PT_GNU_SFRAME
+#define PT_GNU_SFRAME 0x6474e554
+#endif
+
 static inline void __attribute__ ((always_inline))
 setup_vdso (struct link_map *main_map __attribute__ ((unused)),
 	    struct link_map ***first_preload __attribute__ ((unused)))
@@ -52,6 +57,14 @@ setup_vdso (struct link_map *main_map __attribute__ ((unused)),
 	      if (ph->p_vaddr + ph->p_memsz >= l->l_map_end)
 		l->l_map_end = ph->p_vaddr + ph->p_memsz;
 	    }
+	  else if (ph->p_type == PT_GNU_SFRAME)
+	    {
+	      if (! l->l_sframe_start)
+		{
+		  l->l_sframe_start = ph->p_vaddr;
+		  l->l_sframe_end   = ph->p_vaddr + ph->p_memsz;
+		}
+	    }
 	  else
 	    /* There must be no TLS segment.  */
 	    assert (ph->p_type != PT_TLS);
@@ -74,6 +87,15 @@ setup_vdso (struct link_map *main_map __attribute__ ((unused)),
       l->l_local_scope[0]->r_nlist = 1;
       l->l_local_scope[0]->r_list = &l->l_real;
 
+#define PR_ADD_SFRAME 77
+      if (l->l_sframe_start != 0)
+	{
+	  l->l_sframe_start += l->l_addr;
+	  l->l_sframe_end   += l->l_addr;
+
+	  __prctl(PR_ADD_SFRAME, l->l_sframe_start, l->l_sframe_end, l->l_addr, l->l_map_end);
+	}
+
       /* Now that we have the info handy, use the DSO image's soname
 	 so this object can be looked up by name.  */
       if (l->l_info[DT_SONAME] != NULL)
include/link.h +3 -0
index 5ed445d5a6..e94390b29e 100644
@@ -345,6 +345,9 @@ struct link_map
     ElfW(Addr) l_relro_addr;
     size_t l_relro_size;
 
+    ElfW(Addr) l_sframe_start;
+    ElfW(Addr) l_sframe_end;
+
     unsigned long long int l_serial;
   };
 
maintainer
#160 lore raw
In reply to Josh Poimboeuf
Quoted 11 lines · On Tue, 21 Jan 2025 18:30:52 -0800 Josh Poimboeuf <[email protected]> wrote:

The interface is similar to {task,irq}_work. The caller owns an unwind_work struct:

  struct unwind_work {
	struct callback_head		work;
	unwind_callback_t		func;
	int				pending;

};

For perf, struct unwind_work is embedded in struct perf_event. For ftrace maybe it would live in task_struct?

Hmm, this is going to be difficult, as I don't want to add more to a task struct as it's already too bloated as is. I'll have to think about this a bit.

-- Steve