Allow unprivileged users to trace their own processes' syscalls using
perf trace, similar to strace without the overhead of ptrace().

Currently, perf trace requires CAP_PERFMON or paranoid level ≤ 1 even
though the kernel has existing infrastructure (TRACE_EVENT_FL_CAP_ANY)
designed to mark syscall tracepoints as safe for unprivileged access.
To fix this:

1. Loosen the condition in perf_event_open() which requires privileges
   for all events with exclude_kernel=0. This allows perf_event_open() to
   bypass the paranoid check for task-attached tracepoint events. Ensure
   that sample types which can expose kernel addresses to unprivileged
   users are blocked. Ensure the PERF_SECURITY_KERNEL LSM hook is
   preserved.

2. Add a check to perf_trace_event_perm() to block PERF_SAMPLE_IP on
   kernel tracepoints for unprivileged users to prevent KASLR bypass. We do
   this here rather than in kaddr_leak because perf_trace_event_perm() can
   distinguish between kernel tracepoints and uprobe tracepoints, where the
   IP is a safe user space address and is necessary for uprobe
   functionality.

3. Restrict pure counting events (no PERF_SAMPLE_RAW) to
   TRACE_EVENT_FL_CAP_ANY tracepoints preventing unprivileged users from
   counting internal kernel tracepoints while preserving current
   behavior for exclude_kernel=1 events.

Example usage after this change:
  $ perf trace ls          # works as unprivileged user
  $ perf trace             # system-wide, still requires privileges
  $ perf trace -p 1234     # requires ptrace permission on pid 1234

Assisted-by: CLAUDE:claude-opus-4 Apogee
Signed-off-by: Anubhav Shelat <[email protected]>
---
 kernel/events/core.c            | 28 +++++++++++++++++++++++++---
 kernel/trace/trace_event_perf.c | 28 +++++++++++++++++++++++++++-
 2 files changed, 52 insertions(+), 4 deletions(-)

diff --git a/kernel/events/core.c b/kernel/events/core.c
index 954c36e28101..48bfff07ae02 100644
--- a/kernel/events/core.c
+++ b/kernel/events/core.c
@@ -13910,9 +13910,31 @@ SYSCALL_DEFINE5(perf_event_open,
                return err;
 
        if (!attr.exclude_kernel) {
-               err = perf_allow_kernel();
-               if (err)
-                       return err;
+               bool tp_bypass = false;
+
+               /* Check unprivileged tracepoints */
+               if (attr.type == PERF_TYPE_TRACEPOINT && pid != -1) {
+                       /*
+                        * Block sample types that expose kernel addresses to
+                        * prevent KASLR bypass
+                        */
+                       u64 kaddr_leak = PERF_SAMPLE_CALLCHAIN |
+                                        PERF_SAMPLE_BRANCH_STACK |
+                                        PERF_SAMPLE_ADDR |
+                                        PERF_SAMPLE_REGS_INTR;
+
+                       tp_bypass = !(attr.sample_type & kaddr_leak);
+               }
+
+               if (!tp_bypass) {
+                       err = perf_allow_kernel();
+                       if (err)
+                               return err;
+               } else {
+                       err = security_perf_event_open(PERF_SECURITY_KERNEL);
+                       if (err)
+                               return err;
+               }
        }
 
        if (attr.namespaces) {
diff --git a/kernel/trace/trace_event_perf.c b/kernel/trace/trace_event_perf.c
index 5b272856e5ab..a264154b460e 100644
--- a/kernel/trace/trace_event_perf.c
+++ b/kernel/trace/trace_event_perf.c
@@ -24,6 +24,16 @@ typedef typeof(unsigned long [PERF_MAX_TRACE_SIZE / 
sizeof(unsigned long)])
 /* Count the events in use (per event id, not per instance) */
 static int     total_ref_count;
 
+/* Check if perf tracepoint is restricted for unprivileged users */
+static bool perf_tp_is_restricted(struct perf_event *p_event)
+{
+       if (p_event->attr.exclude_kernel)
+               return false;
+       if (sysctl_perf_event_paranoid <= 1 || perfmon_capable())
+               return false;
+       return true;
+}
+
 static int perf_trace_event_perm(struct trace_event_call *tp_event,
                                 struct perf_event *p_event)
 {
@@ -72,9 +82,25 @@ static int perf_trace_event_perm(struct trace_event_call 
*tp_event,
                        return -EINVAL;
        }
 
+       /*
+        * PERF_SAMPLE_IP on kernel tracepoints exposes a kernel text
+        * address, weakening KASLR. Block for unprivileged users unless
+        * the tracepoint is a uprobe (userspace IP, safe to expose).
+        */
+       if ((p_event->attr.sample_type & PERF_SAMPLE_IP) &&
+           !(tp_event->flags & TRACE_EVENT_FL_UPROBE) &&
+           perf_tp_is_restricted(p_event))
+               return -EACCES;
+
        /* No tracing, just counting, so no obvious leak */
-       if (!(p_event->attr.sample_type & PERF_SAMPLE_RAW))
+       if (!(p_event->attr.sample_type & PERF_SAMPLE_RAW)) {
+               /* Prevent unprivileged users from counting kernel tracepoints 
*/
+               if (perf_tp_is_restricted(p_event) &&
+                   !(p_event->attach_state == PERF_ATTACH_TASK &&
+                     (tp_event->flags & TRACE_EVENT_FL_CAP_ANY)))
+                       return -EACCES;
                return 0;
+       }
 
        /* Some events are ok to be traced by non-root users... */
        if (p_event->attach_state == PERF_ATTACH_TASK) {
-- 
2.54.0


Reply via email to