* Re: [PATCH v2] net: stmmac: fix RX DMA leak on TX alloc failure
From: Paolo Abeni @ 2026-05-26 10:49 UTC (permalink / raw)
To: Abid Ali, devnull+dev.taqnialabs.gmail.com
Cc: alexandre.torgue, andrew+netdev, davem, edumazet, kuba,
linux-arm-kernel, linux-kernel, linux-stm32, mcoquelin.stm32,
netdev
In-Reply-To: <20260523121708.564-1-dev.taqnialabs@gmail.com>
On 5/23/26 2:17 PM, Abid Ali wrote:
>> ret = alloc_dma_tx_desc_resources(priv, dma_conf);
>> + if (ret)
>> + free_dma_rx_desc_resources(priv, dma_conf);
>>
>> return ret;
>> }
>
> The sashiko-gemini analysis [1] flagged two issues.
>
> 1) Double-free via XDP path:
>
> stmmac_xdp_set_prog() ignores the return of stmmac_xdp_open(), so
> if alloc_dma_tx_desc_resources() fails inside that path,
> rx_q->buf_pool and rx_q->dma_rx are freed for Rx queues.
>
> The interface stays UP, so a later stmmac_release() calls
> free_dma_desc_resources() on the same freed pointers.
>
> Without this patch, the same failure path leaks RX resources
> instead. Either way the root cause seems to be stmmac_xdp_set_prog() not
> handling errors from stmmac_xdp_open().
>
> The reported issue seems to be valid, but I'm not sure why XDP doesn't handle
> a possible error in reinit in the first place.
>
> 2) NULL deref on partial queue alloc:
>
> If alloc_dma_rx_desc_resources() fails for queue N,
> e.g. rx_q->page_pool = page_pool_create() fails, buf_pool is NULL.
> The cleanup free_dma_rx_desc_resources() iterates through all
> queues and will hit a NULL pointer deref in:
>
> static void stmmac_free_rx_buffer(struct stmmac_priv *priv,
> struct stmmac_rx_queue *rx_q,
> int i)
> {
> struct stmmac_rx_buffer *buf = &rx_q->buf_pool[i];
>
> The same could happen without the patch, and similar risk exists for
> rx_q->buf_pool, rx_q->dma_rx, and rx_q->dma_erx which are all freed
> without guards in __free_dma_rx_desc_resources().
>
> I can add the necessary NULL guards in __free_dma_rx_desc_resources()
> for V3 if necessary.
My take is following: even if the sashiko reported issues are
pre-existing, they are so strictly tied to this code path that it makes
sense to address all of them together (different patches in the same
series).
Also all of the above looks like 'net' material, deserving fixes tag in
each patch.
/P
^ permalink raw reply
* Re: [PATCH] firmware: arm_ffa: Treat missing FF-A feature on a platform as a probe miss
From: Yeoreum Yun @ 2026-05-26 10:51 UTC (permalink / raw)
To: Sudeep Holla
Cc: linux-security-module, linux-kernel, linux-integrity,
linux-arm-kernel, kvmarm, Nathan Chancellor
In-Reply-To: <20260526103649.5684-1-sudeep.holla@kernel.org>
LGTM.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
On Tue, May 26, 2026 at 11:36:49AM +0100, Sudeep Holla wrote:
> When FF-A initialisation is driven from a platform device probe, systems
> that do not implement FF-A can return -EOPNOTSUPP from the early transport
> or version discovery paths. Driver core treats that as a matched probe
> failure and prints:
>
> | arm-ffa arm-ffa: probe with driver arm-ffa failed with error -95
>
> That is noisy for a firmware interface that can be absent on otherwise
> valid systems. Driver core already treats -ENODEV and -ENXIO as quiet
> rejected matches, so translate only the early unsupported discovery cases
> to -ENODEV. Keep later setup failures unchanged so real FF-A
> initialisation problems are still reported as probe failures.
>
> Reported-by: Nathan Chancellor <nathan@kernel.org>
> Closes: https://lore.kernel.org/all/20260523001148.GA1319283@ax162
> Signed-off-by: Sudeep Holla <sudeep.holla@kernel.org>
> ---
> drivers/firmware/arm_ffa/driver.c | 7 +++++--
> 1 file changed, 5 insertions(+), 2 deletions(-)
>
> diff --git a/drivers/firmware/arm_ffa/driver.c b/drivers/firmware/arm_ffa/driver.c
> index 54984e1b9741..0f468362c288 100644
> --- a/drivers/firmware/arm_ffa/driver.c
> +++ b/drivers/firmware/arm_ffa/driver.c
> @@ -2109,7 +2109,7 @@ static int ffa_probe(struct platform_device *pdev)
>
> ret = ffa_transport_init(&invoke_ffa_fn);
> if (ret)
> - return ret;
> + return ret == -EOPNOTSUPP ? -ENODEV : ret;
>
> drv_info = kzalloc_obj(*drv_info);
> if (!drv_info)
> @@ -2117,8 +2117,11 @@ static int ffa_probe(struct platform_device *pdev)
> platform_set_drvdata(pdev, drv_info);
>
> ret = ffa_version_check(&drv_info->version);
> - if (ret)
> + if (ret) {
> + if (ret == -EOPNOTSUPP)
> + ret = -ENODEV;
> goto free_drv_info;
> + }
>
> if (ffa_id_get(&drv_info->vm_id)) {
> pr_err("failed to obtain VM id for self\n");
> --
> 2.43.0
>
--
Sincerely,
Yeoreum Yun
^ permalink raw reply
* Re: [PATCH 2/2] gpio: rockchip: fix teardown bugs and resource leaks
From: Bartosz Golaszewski @ 2026-05-26 10:53 UTC (permalink / raw)
To: Marco Scardovi (scardracs)
Cc: linusw, heiko, linux-gpio, linux-rockchip, linux-arm-kernel,
linux-kernel
In-Reply-To: <20260525164230.43307-3-scardracs@disroot.org>
On Mon, May 25, 2026 at 6:42 PM Marco Scardovi (scardracs)
<scardracs@disroot.org> wrote:
>
> This commit addresses several teardown issues and resource leaks in the
> driver's remove path and error handling:
>
> 1. Debounce clock reference leak: The debounce clock (bank->db_clk) was
> obtained using of_clk_get() which increments the clock's reference
> count, but clk_put() was never called. A devm action is now registered
> to cleanly release it on unbind. Note that of_clk_get(..., 1) remains
> necessary over devm_clk_get() because the DT binding does not define
> clock-names, precluding name-based lookup.
>
> 2. Unregistered chained IRQ handler: The chained IRQ handler was not
> disconnected in remove(). If a stray interrupt fired after the driver
> was removed, the kernel would attempt to execute a stale handler,
> leading to a panic. This is fixed by clearing the handler in remove().
>
> 3. IRQ domain leak: The linear IRQ domain and its generic chips were
> allocated manually during probe but never removed. The IRQ domain is
> now removed during driver teardown to free the associated generic chips
> and mappings.
>
I suppose the description comes from the LMM. Please use imperative
mode as is customary in kernel commit messages.
> Assisted-by: Antigravity:gemini-3.5-flash
> Signed-off-by: Marco Scardovi <scardracs@disroot.org>
> ---
> drivers/gpio/gpio-rockchip.c | 19 ++++++++++++++++++-
> 1 file changed, 18 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/gpio/gpio-rockchip.c b/drivers/gpio/gpio-rockchip.c
> index 33580093a4e7..c804f970d823 100644
> --- a/drivers/gpio/gpio-rockchip.c
> +++ b/drivers/gpio/gpio-rockchip.c
> @@ -638,10 +638,17 @@ static int rockchip_gpiolib_register(struct rockchip_pin_bank *bank)
> return ret;
> }
>
> +static void rockchip_clk_put(void *data)
> +{
> + struct clk *clk = data;
> +
> + clk_put(clk);
> +}
> +
> static int rockchip_get_bank_data(struct rockchip_pin_bank *bank)
> {
> struct resource res;
> - int id = 0;
> + int id = 0, ret;
>
> if (of_address_to_resource(bank->of_node, 0, &res)) {
> dev_err(bank->dev, "cannot find IO resource for bank\n");
> @@ -673,6 +680,13 @@ static int rockchip_get_bank_data(struct rockchip_pin_bank *bank)
> dev_err(bank->dev, "cannot find debounce clk\n");
> return -EINVAL;
> }
> +
> + ret = devm_add_action_or_reset(bank->dev, rockchip_clk_put,
> + bank->db_clk);
Is there any reason to not convert his call into a devm_clk_get() variant too?
> + if (ret) {
> + dev_err(bank->dev, "failed to register debounce clk action\n");
> + return ret;
> + }
> break;
> case GPIO_TYPE_V1:
> bank->gpio_regs = &gpio_regs_v1;
> @@ -789,6 +803,9 @@ static void rockchip_gpio_remove(struct platform_device *pdev)
> {
> struct rockchip_pin_bank *bank = platform_get_drvdata(pdev);
>
> + irq_set_chained_handler_and_data(bank->irq, NULL, NULL);
> + if (bank->domain)
> + irq_domain_remove(bank->domain);
> gpiochip_remove(&bank->gpio_chip);
> }
>
> --
> 2.54.0
>
>
Bart
^ permalink raw reply
* RE: [PATCH] [net-next] net: dsa: netc: fix enetc dependencies
From: Wei Fang @ 2026-05-26 11:03 UTC (permalink / raw)
To: Arnd Bergmann, Claudiu Manoil, Clark Wang,
Christophe Leroy (CS GROUP)
Cc: Arnd Bergmann, Andrew Lunn, Vladimir Oltean, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, imx@lists.linux.dev,
netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
linuxppc-dev@lists.ozlabs.org,
linux-arm-kernel@lists.infradead.org
In-Reply-To: <20260526102708.2837129-1-arnd@kernel.org>
> Add the required 'NET_VENDOR_FREESCALE' dependency to make it possible
> to select both the PHY and NTMP library modules. Originally this was
> meant to be done through an 'IS_REACHABLE' check in the header file,
> but that did not work because the drivers/net/ethernet/freescale
> directory is not even entered in this case. IS_REACHABLE() is generally
> counterproductive because even when it works as intended, it turns
> a helpful link-time error into a silent runtime failure that is
> harder to debug. In this case, it clearly did not even do anything.
>
Hi Arnd,
Thanks for fix this issue, I have sent a patch last Sunday.
Link: https://lore.kernel.org/netdev/20260524070310.2429819-1-wei.fang@nxp.com/
I think the solution should simply be to add
"depends on NET_VENDOR_FREESCALE", right? The changes in
enetc_mdio.h seem more like improvements to me.
^ permalink raw reply
* [PATCH 0/2] perf cs-etm: Queue context packets for frontend
From: James Clark @ 2026-05-26 11:14 UTC (permalink / raw)
To: Suzuki K Poulose, Mike Leach, Leo Yan, Peter Zijlstra,
Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon, Amir Ayupov
Cc: coresight, linux-arm-kernel, linux-perf-users, linux-kernel,
James Clark
Fix thread tracking when decoding Coresight trace and add a new test for
it.
Unfortunately the test has to be added as a separate binary like the
other existing Coresight workloads which needs a bit of makefile
boilerplate. Ideally it would be a builtin Perf test workload, but Perf
has a lot of dependencies and generates a lot of trace when starting up
which makes tracing tests very slow because all that has to be decoded.
Hopefully I can find a generic way to enable Perf events at the
beginning of the workload and then I can migrate everything from
tools/perf/tests/shell/coresight to Perf test workloads, but that will
have to be done as a separate change.
Signed-off-by: James Clark <james.clark@linaro.org>
---
James Clark (1):
perf test cs-etm: Test thread attribution
Leo Yan (1):
perf cs-etm: Queue context packets for frontend
tools/perf/tests/shell/coresight/Makefile | 1 +
.../shell/coresight/context_switch_loop/.gitignore | 1 +
.../shell/coresight/context_switch_loop/Makefile | 29 ++++
.../context_switch_loop/context_switch_loop.c | 83 ++++++++++
.../tests/shell/coresight/context_switch_thread.sh | 48 ++++++
tools/perf/util/cs-etm-decoder/cs-etm-decoder.c | 18 +-
tools/perf/util/cs-etm.c | 181 +++++++++++++--------
tools/perf/util/cs-etm.h | 5 +-
8 files changed, 295 insertions(+), 71 deletions(-)
---
base-commit: 09d355618f7ccc27ffc7fc668b2e232872962079
change-id: 20260515-james-cs-context-tracking-fix-754998bae7ed
Best regards,
--
James Clark <james.clark@linaro.org>
^ permalink raw reply
* [PATCH 1/2] perf cs-etm: Queue context packets for frontend
From: James Clark @ 2026-05-26 11:14 UTC (permalink / raw)
To: Suzuki K Poulose, Mike Leach, Leo Yan, Peter Zijlstra,
Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon, Amir Ayupov
Cc: coresight, linux-arm-kernel, linux-perf-users, linux-kernel,
James Clark
In-Reply-To: <20260526-james-cs-context-tracking-fix-v1-0-ebd602e18287@linaro.org>
From: Leo Yan <leo.yan@arm.com>
PE_CONTEXT elements update the context ID and exception level, but the
decoder may still have prior packets cached for frontend processing.
Updating the context immediately in the decoder backend can make those
cached packets get consumed with the wrong thread or EL state.
Add a CS_ETM_CONTEXT packet carrying the TID and EL to the frontend,
this keeps context changes ordered with the rest of the packet stream
and avoids mismatches when synthesizing samples from cached packets.
Separate the memory access function into one for the frontend and one
for decoding. The frontend also needs memory access to attach the
instruction to samples.
Treat context packets as a boundary for branch sample generation and
remove tidq->prev_packet_thread because it's not possible to branch to a
different thread, so only tracking the current thread is required for
sample generation.
Reported-by: Amir Ayupov <aaupov@meta.com>
Closes: https://lore.kernel.org/linux-perf-users/20260515021135.1729028-1-aaupov@meta.com/
Co-authored-by: James Clark <james.clark@linaro.org>
Signed-off-by: Leo Yan <leo.yan@arm.com>
---
tools/perf/util/cs-etm-decoder/cs-etm-decoder.c | 18 ++-
tools/perf/util/cs-etm.c | 181 +++++++++++++++---------
tools/perf/util/cs-etm.h | 5 +-
3 files changed, 133 insertions(+), 71 deletions(-)
diff --git a/tools/perf/util/cs-etm-decoder/cs-etm-decoder.c b/tools/perf/util/cs-etm-decoder/cs-etm-decoder.c
index dee3020ceaa9..0b9e316b3bf4 100644
--- a/tools/perf/util/cs-etm-decoder/cs-etm-decoder.c
+++ b/tools/perf/util/cs-etm-decoder/cs-etm-decoder.c
@@ -525,6 +525,8 @@ cs_etm_decoder__set_tid(struct cs_etm_queue *etmq,
const ocsd_generic_trace_elem *elem,
const uint8_t trace_chan_id)
{
+ struct cs_etm_packet *packet;
+ int ret;
pid_t tid = -1;
/*
@@ -546,12 +548,18 @@ cs_etm_decoder__set_tid(struct cs_etm_queue *etmq,
break;
}
- if (cs_etm__etmq_set_tid_el(etmq, tid, trace_chan_id,
- elem->context.exception_level))
+ if (cs_etm__etmq_set_decode_tid_el(etmq, tid, trace_chan_id,
+ elem->context.exception_level))
return OCSD_RESP_FATAL_SYS_ERR;
- if (tid == -1)
- return OCSD_RESP_CONT;
+ ret = cs_etm_decoder__buffer_packet(etmq, packet_queue, trace_chan_id,
+ CS_ETM_CONTEXT);
+ if (ret != OCSD_RESP_CONT && ret != OCSD_RESP_WAIT)
+ return ret;
+
+ packet = &packet_queue->packet_buffer[packet_queue->tail];
+ packet->tid = tid;
+ packet->el = elem->context.exception_level;
/*
* A timestamp is generated after a PE_CONTEXT element so make sure
@@ -559,7 +567,7 @@ cs_etm_decoder__set_tid(struct cs_etm_queue *etmq,
*/
cs_etm_decoder__reset_timestamp(packet_queue);
- return OCSD_RESP_CONT;
+ return ret;
}
static ocsd_datapath_resp_t cs_etm_decoder__gen_trace_elem_printer(
diff --git a/tools/perf/util/cs-etm.c b/tools/perf/util/cs-etm.c
index 8a639d2e51a4..e22196418e8a 100644
--- a/tools/perf/util/cs-etm.c
+++ b/tools/perf/util/cs-etm.c
@@ -85,10 +85,11 @@ struct cs_etm_traceid_queue {
u64 period_instructions;
size_t last_branch_pos;
union perf_event *event_buf;
- struct thread *thread;
- struct thread *prev_packet_thread;
- ocsd_ex_level prev_packet_el;
- ocsd_ex_level el;
+ struct thread *frontend_thread;
+ struct thread *decode_thread;
+ ocsd_ex_level prev_frontend_el;
+ ocsd_ex_level frontend_el;
+ ocsd_ex_level decode_el;
struct branch_stack *last_branch;
struct branch_stack *last_branch_rb;
struct cs_etm_packet *prev_packet;
@@ -614,10 +615,12 @@ static int cs_etm__init_traceid_queue(struct cs_etm_queue *etmq,
queue = &etmq->etm->queues.queue_array[etmq->queue_nr];
tidq->trace_chan_id = trace_chan_id;
- tidq->el = tidq->prev_packet_el = ocsd_EL_unknown;
- tidq->thread = machine__findnew_thread(&etm->session->machines.host, -1,
+ tidq->frontend_el = tidq->decode_el = ocsd_EL_unknown;
+ tidq->frontend_thread = machine__findnew_thread(&etm->session->machines.host, -1,
queue->tid);
- tidq->prev_packet_thread = machine__idle_thread(&etm->session->machines.host);
+ tidq->decode_thread = machine__findnew_thread(&etm->session->machines.host, -1,
+ queue->tid);
+
tidq->packet = zalloc(sizeof(struct cs_etm_packet));
if (!tidq->packet)
@@ -751,20 +754,17 @@ static void cs_etm__packet_swap(struct cs_etm_auxtrace *etm,
* Swap PACKET with PREV_PACKET: PACKET becomes PREV_PACKET for
* the next incoming packet.
*
- * Threads and exception levels are also tracked for both the
- * previous and current packets. This is because the previous
- * packet is used for the 'from' IP for branch samples, so the
- * thread at that time must also be assigned to that sample.
- * Across discontinuity packets the thread can change, so by
- * tracking the thread for the previous packet the branch sample
- * will have the correct info.
+ * Track Exception levels for both the previous and current
+ * packets. This is because the previous packet's address is
+ * used for the 'from' IP for branch samples, so the previous EL
+ * must also be used so that sample shows it originates from the
+ * correct EL. Branches can't branch to a different thread, so
+ * no need to track the previous thread.
*/
tmp = tidq->packet;
tidq->packet = tidq->prev_packet;
tidq->prev_packet = tmp;
- tidq->prev_packet_el = tidq->el;
- thread__put(tidq->prev_packet_thread);
- tidq->prev_packet_thread = thread__get(tidq->thread);
+ tidq->prev_frontend_el = tidq->frontend_el;
}
}
@@ -937,8 +937,8 @@ static void cs_etm__free_traceid_queues(struct cs_etm_queue *etmq)
/* Free this traceid_queue from the array */
tidq = etmq->traceid_queues[idx];
- thread__zput(tidq->thread);
- thread__zput(tidq->prev_packet_thread);
+ thread__zput(tidq->frontend_thread);
+ thread__zput(tidq->decode_thread);
zfree(&tidq->event_buf);
zfree(&tidq->last_branch);
zfree(&tidq->last_branch_rb);
@@ -1083,9 +1083,10 @@ static u8 cs_etm__cpu_mode(struct cs_etm_queue *etmq, u64 address,
}
}
-static u32 cs_etm__mem_access(struct cs_etm_queue *etmq, u8 trace_chan_id,
- u64 address, size_t size, u8 *buffer,
- const ocsd_mem_space_acc_t mem_space)
+static u32 __cs_etm__mem_access(struct cs_etm_queue *etmq, u8 trace_chan_id,
+ u64 address, size_t size, u8 *buffer,
+ const ocsd_mem_space_acc_t mem_space,
+ bool is_decoder)
{
u8 cpumode;
u64 offset;
@@ -1094,6 +1095,8 @@ static u32 cs_etm__mem_access(struct cs_etm_queue *etmq, u8 trace_chan_id,
struct dso *dso;
struct cs_etm_traceid_queue *tidq;
int ret = 0;
+ ocsd_ex_level el;
+ struct thread *thread;
if (!etmq)
return 0;
@@ -1103,27 +1106,35 @@ static u32 cs_etm__mem_access(struct cs_etm_queue *etmq, u8 trace_chan_id,
if (!tidq)
goto out;
+ if (is_decoder) {
+ el = tidq->decode_el;
+ thread = tidq->decode_thread;
+ } else {
+ el = tidq->frontend_el;
+ thread = tidq->frontend_thread;
+ }
+
/*
- * We've already tracked EL along side the PID in cs_etm__set_thread()
- * so double check that it matches what OpenCSD thinks as well. It
- * doesn't distinguish between EL0 and EL1 for this mem access callback
- * so we had to do the extra tracking. Skip validation if it's any of
- * the 'any' values.
+ * We've already tracked EL along side the PID in
+ * cs_etm__etmq_set_[decode/frontend]_tid_el() so double check that it
+ * matches what OpenCSD thinks as well. OpenCSD doesn't distinguish
+ * between EL0 and EL1 for this mem access callback so we had to do the
+ * extra tracking. Skip validation if it's any of the 'any' values.
*/
if (!(mem_space == OCSD_MEM_SPACE_ANY ||
mem_space == OCSD_MEM_SPACE_N || mem_space == OCSD_MEM_SPACE_S)) {
if (mem_space & OCSD_MEM_SPACE_EL1N) {
/* Includes both non secure EL1 and EL0 */
- assert(tidq->el == ocsd_EL1 || tidq->el == ocsd_EL0);
+ assert(el == ocsd_EL1 || el == ocsd_EL0);
} else if (mem_space & OCSD_MEM_SPACE_EL2)
- assert(tidq->el == ocsd_EL2);
+ assert(el == ocsd_EL2);
else if (mem_space & OCSD_MEM_SPACE_EL3)
- assert(tidq->el == ocsd_EL3);
+ assert(el == ocsd_EL3);
}
- cpumode = cs_etm__cpu_mode(etmq, address, tidq->el);
+ cpumode = cs_etm__cpu_mode(etmq, address, el);
- if (!thread__find_map(tidq->thread, cpumode, address, &al))
+ if (!thread__find_map(thread, cpumode, address, &al))
goto out;
dso = map__dso(al.map);
@@ -1138,7 +1149,7 @@ static u32 cs_etm__mem_access(struct cs_etm_queue *etmq, u8 trace_chan_id,
map__load(al.map);
- len = dso__data_read_offset(dso, maps__machine(thread__maps(tidq->thread)),
+ len = dso__data_read_offset(dso, maps__machine(thread__maps(thread)),
offset, buffer, size);
if (len <= 0) {
@@ -1158,6 +1169,20 @@ static u32 cs_etm__mem_access(struct cs_etm_queue *etmq, u8 trace_chan_id,
return ret;
}
+static u32 cs_etm__frontend_mem_access(struct cs_etm_queue *etmq, u8 trace_chan_id,
+ u64 address, size_t size, u8 *buffer)
+{
+ return __cs_etm__mem_access(etmq, trace_chan_id, address, size, buffer,
+ OCSD_MEM_SPACE_NONE, false);
+}
+static u32 cs_etm__decoder_mem_access(struct cs_etm_queue *etmq, u8 trace_chan_id,
+ u64 address, size_t size, u8 *buffer,
+ const ocsd_mem_space_acc_t mem_space)
+{
+ return __cs_etm__mem_access(etmq, trace_chan_id, address, size, buffer,
+ mem_space, true);
+}
+
static struct cs_etm_queue *cs_etm__alloc_queue(void)
{
struct cs_etm_queue *etmq = zalloc(sizeof(*etmq));
@@ -1337,8 +1362,8 @@ static inline int cs_etm__t32_instr_size(struct cs_etm_queue *etmq,
{
u8 instrBytes[2];
- cs_etm__mem_access(etmq, trace_chan_id, addr, ARRAY_SIZE(instrBytes),
- instrBytes, 0);
+ cs_etm__frontend_mem_access(etmq, trace_chan_id, addr,
+ ARRAY_SIZE(instrBytes), instrBytes);
/*
* T32 instruction size is indicated by bits[15:11] of the first
* 16-bit word of the instruction: 0b11101, 0b11110 and 0b11111
@@ -1472,34 +1497,55 @@ cs_etm__get_trace(struct cs_etm_queue *etmq)
return etmq->buf_len;
}
-static void cs_etm__set_thread(struct cs_etm_queue *etmq,
- struct cs_etm_traceid_queue *tidq, pid_t tid,
- ocsd_ex_level el)
+/*
+ * Set the TID and EL from the latest packet pulled out of the
+ * cs_etm_packet_queue. Used for generating samples by the frontend.
+ */
+static int cs_etm__etmq_set_frontend_tid_el(struct cs_etm_queue *etmq,
+ struct cs_etm_traceid_queue *tidq)
{
- struct machine *machine = cs_etm__get_machine(etmq, el);
+ struct cs_etm_packet *packet = tidq->packet;
- if (tid != -1) {
- thread__zput(tidq->thread);
- tidq->thread = machine__find_thread(machine, -1, tid);
+ struct machine *machine = cs_etm__get_machine(etmq, packet->el);
+
+ if (packet->tid != -1) {
+ thread__zput(tidq->frontend_thread);
+ tidq->frontend_thread = machine__find_thread(machine, -1, packet->tid);
}
/* Couldn't find a known thread */
- if (!tidq->thread)
- tidq->thread = machine__idle_thread(machine);
+ if (!tidq->frontend_thread)
+ tidq->frontend_thread = machine__idle_thread(machine);
- tidq->el = el;
+ tidq->frontend_el = packet->el;
+
+ return 0;
}
-int cs_etm__etmq_set_tid_el(struct cs_etm_queue *etmq, pid_t tid,
- u8 trace_chan_id, ocsd_ex_level el)
+/*
+ * Set the TID and EL used to access memory for decoding. This is ahead
+ * of the TID and EL for generating samples
+ */
+int cs_etm__etmq_set_decode_tid_el(struct cs_etm_queue *etmq, pid_t tid,
+ u8 trace_chan_id, ocsd_ex_level el)
{
struct cs_etm_traceid_queue *tidq;
+ struct machine *machine = cs_etm__get_machine(etmq, el);
tidq = cs_etm__etmq_get_traceid_queue(etmq, trace_chan_id);
if (!tidq)
return -EINVAL;
- cs_etm__set_thread(etmq, tidq, tid, el);
+ if (tid != -1) {
+ thread__zput(tidq->decode_thread);
+ tidq->decode_thread = machine__find_thread(machine, -1, tid);
+ }
+
+ /* Couldn't find a known thread */
+ if (!tidq->decode_thread)
+ tidq->decode_thread = machine__idle_thread(machine);
+
+ tidq->decode_el = el;
return 0;
}
@@ -1533,8 +1579,8 @@ static void cs_etm__copy_insn(struct cs_etm_queue *etmq,
else
sample->insn_len = 4;
- cs_etm__mem_access(etmq, trace_chan_id, sample->ip, sample->insn_len,
- (void *)sample->insn, 0);
+ cs_etm__frontend_mem_access(etmq, trace_chan_id, sample->ip,
+ sample->insn_len, (void *)sample->insn);
}
u64 cs_etm__convert_sample_time(struct cs_etm_queue *etmq, u64 cs_timestamp)
@@ -1570,15 +1616,15 @@ static int cs_etm__synth_instruction_sample(struct cs_etm_queue *etmq,
perf_sample__init(&sample, /*all=*/true);
event->sample.header.type = PERF_RECORD_SAMPLE;
- event->sample.header.misc = cs_etm__cpu_mode(etmq, addr, tidq->el);
+ event->sample.header.misc = cs_etm__cpu_mode(etmq, addr, tidq->frontend_el);
event->sample.header.size = sizeof(struct perf_event_header);
/* Set time field based on etm auxtrace config. */
sample.time = cs_etm__resolve_sample_time(etmq, tidq);
sample.ip = addr;
- sample.pid = thread__pid(tidq->thread);
- sample.tid = thread__tid(tidq->thread);
+ sample.pid = thread__pid(tidq->frontend_thread);
+ sample.tid = thread__tid(tidq->frontend_thread);
sample.id = etmq->etm->instructions_id;
sample.stream_id = etmq->etm->instructions_id;
sample.period = period;
@@ -1631,15 +1677,15 @@ static int cs_etm__synth_branch_sample(struct cs_etm_queue *etmq,
event->sample.header.type = PERF_RECORD_SAMPLE;
event->sample.header.misc = cs_etm__cpu_mode(etmq, ip,
- tidq->prev_packet_el);
+ tidq->prev_frontend_el);
event->sample.header.size = sizeof(struct perf_event_header);
/* Set time field based on etm auxtrace config. */
sample.time = cs_etm__resolve_sample_time(etmq, tidq);
sample.ip = ip;
- sample.pid = thread__pid(tidq->prev_packet_thread);
- sample.tid = thread__tid(tidq->prev_packet_thread);
+ sample.pid = thread__pid(tidq->frontend_thread);
+ sample.tid = thread__tid(tidq->frontend_thread);
sample.addr = cs_etm__first_executed_instr(tidq->packet);
sample.id = etmq->etm->branches_id;
sample.stream_id = etmq->etm->branches_id;
@@ -1874,7 +1920,8 @@ static int cs_etm__sample(struct cs_etm_queue *etmq,
bool generate_sample = false;
/* Generate sample for tracing on packet */
- if (tidq->prev_packet->sample_type == CS_ETM_DISCONTINUITY)
+ if (tidq->prev_packet->sample_type == CS_ETM_DISCONTINUITY ||
+ tidq->prev_packet->sample_type == CS_ETM_CONTEXT)
generate_sample = true;
/* Generate sample for branch taken packet */
@@ -2057,8 +2104,8 @@ static bool cs_etm__is_svc_instr(struct cs_etm_queue *etmq, u8 trace_chan_id,
* so below only read 2 bytes as instruction size for T32.
*/
addr = end_addr - 2;
- cs_etm__mem_access(etmq, trace_chan_id, addr, sizeof(instr16),
- (u8 *)&instr16, 0);
+ cs_etm__frontend_mem_access(etmq, trace_chan_id, addr,
+ sizeof(instr16), (u8 *)&instr16);
if ((instr16 & 0xFF00) == 0xDF00)
return true;
@@ -2073,8 +2120,8 @@ static bool cs_etm__is_svc_instr(struct cs_etm_queue *etmq, u8 trace_chan_id,
* +---------+---------+-------------------------+
*/
addr = end_addr - 4;
- cs_etm__mem_access(etmq, trace_chan_id, addr, sizeof(instr32),
- (u8 *)&instr32, 0);
+ cs_etm__frontend_mem_access(etmq, trace_chan_id, addr,
+ sizeof(instr32), (u8 *)&instr32);
if ((instr32 & 0x0F000000) == 0x0F000000 &&
(instr32 & 0xF0000000) != 0xF0000000)
return true;
@@ -2090,8 +2137,8 @@ static bool cs_etm__is_svc_instr(struct cs_etm_queue *etmq, u8 trace_chan_id,
* +-----------------------+---------+-----------+
*/
addr = end_addr - 4;
- cs_etm__mem_access(etmq, trace_chan_id, addr, sizeof(instr32),
- (u8 *)&instr32, 0);
+ cs_etm__frontend_mem_access(etmq, trace_chan_id, addr,
+ sizeof(instr32), (u8 *)&instr32);
if ((instr32 & 0xFFE0001F) == 0xd4000001)
return true;
@@ -2376,6 +2423,7 @@ static int cs_etm__set_sample_flags(struct cs_etm_queue *etmq,
PERF_IP_FLAG_RETURN |
PERF_IP_FLAG_INTERRUPT;
break;
+ case CS_ETM_CONTEXT:
case CS_ETM_EMPTY:
default:
break;
@@ -2451,6 +2499,9 @@ static int cs_etm__process_traceid_queue(struct cs_etm_queue *etmq,
*/
cs_etm__sample(etmq, tidq);
break;
+ case CS_ETM_CONTEXT:
+ cs_etm__etmq_set_frontend_tid_el(etmq, tidq);
+ break;
case CS_ETM_EXCEPTION:
case CS_ETM_EXCEPTION_RET:
/*
@@ -2602,7 +2653,7 @@ static int cs_etm__process_timeless_queues(struct cs_etm_auxtrace *etm,
if (!tidq)
continue;
- if (tid == -1 || thread__tid(tidq->thread) == tid)
+ if (tid == -1 || thread__tid(tidq->frontend_thread) == tid)
cs_etm__run_per_thread_timeless_decoder(etmq);
} else
cs_etm__run_per_cpu_timeless_decoder(etmq);
@@ -3310,7 +3361,7 @@ static int cs_etm__create_queue_decoders(struct cs_etm_queue *etmq)
*/
if (cs_etm_decoder__add_mem_access_cb(etmq->decoder,
0x0L, ((u64) -1L),
- cs_etm__mem_access))
+ cs_etm__decoder_mem_access))
goto out_free_decoder;
zfree(&t_params);
diff --git a/tools/perf/util/cs-etm.h b/tools/perf/util/cs-etm.h
index aa9bb4a32eca..0c6dac9ebd11 100644
--- a/tools/perf/util/cs-etm.h
+++ b/tools/perf/util/cs-etm.h
@@ -158,6 +158,7 @@ enum cs_etm_sample_type {
CS_ETM_DISCONTINUITY,
CS_ETM_EXCEPTION,
CS_ETM_EXCEPTION_RET,
+ CS_ETM_CONTEXT,
};
enum cs_etm_isa {
@@ -184,6 +185,8 @@ struct cs_etm_packet {
u8 last_instr_size;
u8 trace_chan_id;
int cpu;
+ pid_t tid;
+ int el;
};
#define CS_ETM_PACKET_MAX_BUFFER 1024
@@ -259,7 +262,7 @@ enum cs_etm_pid_fmt {
#include <opencsd/ocsd_if_types.h>
int cs_etm__get_cpu(struct cs_etm_queue *etmq, u8 trace_chan_id, int *cpu);
enum cs_etm_pid_fmt cs_etm__get_pid_fmt(struct cs_etm_queue *etmq);
-int cs_etm__etmq_set_tid_el(struct cs_etm_queue *etmq, pid_t tid,
+int cs_etm__etmq_set_decode_tid_el(struct cs_etm_queue *etmq, pid_t tid,
u8 trace_chan_id, ocsd_ex_level el);
bool cs_etm__etmq_is_timeless(struct cs_etm_queue *etmq);
void cs_etm__etmq_set_traceid_queue_timestamp(struct cs_etm_queue *etmq,
--
2.34.1
^ permalink raw reply related
* [PATCH 2/2] perf test cs-etm: Test thread attribution
From: James Clark @ 2026-05-26 11:14 UTC (permalink / raw)
To: Suzuki K Poulose, Mike Leach, Leo Yan, Peter Zijlstra,
Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon, Amir Ayupov
Cc: coresight, linux-arm-kernel, linux-perf-users, linux-kernel,
James Clark
In-Reply-To: <20260526-james-cs-context-tracking-fix-v1-0-ebd602e18287@linaro.org>
Add a workload that creates two threads that read and write to a pipe so
that every read/write results in the other thread being scheduled. If
this is run on one CPU then the threads will alternate and we can test
that processor trace always attributes symbols to the correct thread and
that the attribution changes at the exact point that the thread switch
happened.
Signed-off-by: James Clark <james.clark@linaro.org>
---
tools/perf/tests/shell/coresight/Makefile | 1 +
.../shell/coresight/context_switch_loop/.gitignore | 1 +
.../shell/coresight/context_switch_loop/Makefile | 29 ++++++++
.../context_switch_loop/context_switch_loop.c | 83 ++++++++++++++++++++++
.../tests/shell/coresight/context_switch_thread.sh | 48 +++++++++++++
5 files changed, 162 insertions(+)
diff --git a/tools/perf/tests/shell/coresight/Makefile b/tools/perf/tests/shell/coresight/Makefile
index fa08fd9a5991..710e742d9def 100644
--- a/tools/perf/tests/shell/coresight/Makefile
+++ b/tools/perf/tests/shell/coresight/Makefile
@@ -6,6 +6,7 @@ include ../../../../../tools/scripts/utilities.mak
SUBDIRS = \
asm_pure_loop \
+ context_switch_loop \
memcpy_thread \
thread_loop \
unroll_loop_thread
diff --git a/tools/perf/tests/shell/coresight/context_switch_loop/.gitignore b/tools/perf/tests/shell/coresight/context_switch_loop/.gitignore
new file mode 100644
index 000000000000..86a7becebba4
--- /dev/null
+++ b/tools/perf/tests/shell/coresight/context_switch_loop/.gitignore
@@ -0,0 +1 @@
+context_switch_loop
diff --git a/tools/perf/tests/shell/coresight/context_switch_loop/Makefile b/tools/perf/tests/shell/coresight/context_switch_loop/Makefile
new file mode 100644
index 000000000000..722afc993cb0
--- /dev/null
+++ b/tools/perf/tests/shell/coresight/context_switch_loop/Makefile
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0
+
+include ../Makefile.miniconfig
+
+BIN=context_switch_loop
+LIB=
+
+all: $(BIN)
+
+$(BIN): $(BIN).c
+ifdef CORESIGHT
+ifeq ($(ARCH),arm64)
+ $(Q)$(CC) $(CFLAGS) $(BIN).c -O0 -o $(BIN) $(LIB)
+endif
+endif
+
+install-tests: all
+ifdef CORESIGHT
+ifeq ($(ARCH),arm64)
+ $(call QUIET_INSTALL, tests) \
+ $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/$(INSTDIR_SUB)/$(BIN)'; \
+ $(INSTALL) $(BIN) '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/$(INSTDIR_SUB)/$(BIN)/$(BIN)'
+endif
+endif
+
+clean:
+ $(Q)$(RM) -f $(BIN)
+
+.PHONY: all clean install-tests
diff --git a/tools/perf/tests/shell/coresight/context_switch_loop/context_switch_loop.c b/tools/perf/tests/shell/coresight/context_switch_loop/context_switch_loop.c
new file mode 100644
index 000000000000..6b7cd11952a2
--- /dev/null
+++ b/tools/perf/tests/shell/coresight/context_switch_loop/context_switch_loop.c
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/compiler.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/prctl.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+static int loops = 100;
+static char buf;
+int work = 1234;
+
+#define write_block(fd) \
+ do { \
+ if (write(fd, &buf, 1) <= 0) \
+ exit(1); \
+ } while (0)
+
+#define read_block(fd) \
+ do { \
+ if (read(fd, &buf, 1) <= 0) \
+ exit(1); \
+ } while (0)
+
+static noinline void thread1(int in_fd, int out_fd)
+{
+ for (int i = 0; i < loops; i++) {
+ read_block(in_fd);
+ work += i * 3;
+ write_block(out_fd);
+ }
+}
+
+static noinline void thread2(int in_fd, int out_fd)
+{
+ for (int i = 0; i < loops; i++) {
+ write_block(out_fd);
+ work += i * 7;
+ read_block(in_fd);
+ }
+}
+
+int main(int argc, char **argv)
+{
+ int a_to_b[2], b_to_a[2];
+ pid_t thread1_pid;
+ int status;
+
+ if (argc > 1) {
+ loops = atoi(argv[1]);
+ if (loops < 0) {
+ fprintf(stderr, "Invalid number of loops: %s\n", argv[1]);
+ return 1;
+ }
+ }
+
+ if (pipe(a_to_b) || pipe(b_to_a)) {
+ perror("Pipe error");
+ return 1;
+ }
+
+ thread1_pid = fork();
+ if (thread1_pid < 0) {
+ perror("Fork error");
+ return 1;
+ }
+
+ if (!thread1_pid) {
+ prctl(PR_SET_NAME, "thread1", 0, 0, 0);
+ thread1(b_to_a[0], a_to_b[1]);
+ exit(0);
+ }
+
+ prctl(PR_SET_NAME, "thread2", 0, 0, 0);
+ thread2(a_to_b[0], b_to_a[1]);
+
+ if (waitpid(thread1_pid, &status, 0) != thread1_pid || !WIFEXITED(status) ||
+ WEXITSTATUS(status))
+ return 1;
+
+ return 0;
+}
diff --git a/tools/perf/tests/shell/coresight/context_switch_thread.sh b/tools/perf/tests/shell/coresight/context_switch_thread.sh
new file mode 100755
index 000000000000..10b4118dff9b
--- /dev/null
+++ b/tools/perf/tests/shell/coresight/context_switch_thread.sh
@@ -0,0 +1,48 @@
+#!/bin/bash -e
+# CoreSight / Context switch thread attribution (exclusive)
+
+# SPDX-License-Identifier: GPL-2.0
+
+TEST="context_switch_loop"
+
+if [ "$(id -u)" != 0 ]; then
+ # Requires root for "-C 0" in record command
+ echo "[Skip] No root permission"
+ exit 2
+fi
+
+# shellcheck source=../lib/coresight.sh
+. "$(dirname $0)"/../lib/coresight.sh
+
+DATA="$DATD/perf-$TEST.data"
+SCRIPT="$DATD/perf-$TEST.script"
+
+check_samples() {
+ owner_samples=$(grep -c "thread1.*thread1" "$SCRIPT" || true)
+ next_samples=$(grep -c "thread2.*thread2" "$SCRIPT" || true)
+
+ if [ "$owner_samples" -eq 0 ] || [ "$next_samples" -eq 0 ]; then
+ err "No samples found"
+ fi
+
+ if grep "thread2.*thread1" "$SCRIPT"; then
+ err "Thread1 symbol was attributed to thread2"
+ fi
+
+ if grep "thread1.*thread2" "$SCRIPT"; then
+ err "Thread2 symbol was attributed to thread1"
+ fi
+}
+
+# Pin to one CPU so the two threads alternate running but record into the
+# same trace buffer.
+perf record -o "$DATA" -e cs_etm/timestamp=0/u -C 0 \
+ -- taskset --cpu-list 0 "$BIN" 20 > /dev/null 2>&1
+
+# Test both instruction and branch sample generation modes.
+perf script -i "$DATA" --itrace=i4 -F comm,pid,tid,ip,sym > "$SCRIPT" 2>/dev/null
+check_samples
+perf script -i "$DATA" --itrace=b -F comm,pid,tid,ip,sym > "$SCRIPT" 2>/dev/null
+check_samples
+
+exit 0
--
2.34.1
^ permalink raw reply related
* [PATCH RFC v8 00/24] pkeys-based page table hardening
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
This is a proposal to leverage protection keys (pkeys) to harden
critical kernel data, by making it mostly read-only. The series includes
a simple framework called "kpkeys" to manipulate pkeys for in-kernel use,
as well as a page table hardening feature based on that framework,
"kpkeys_hardened_pgtables". Both are implemented on arm64 as a proof of
concept, but they are designed to be compatible with any architecture
that supports pkeys.
The proposed approach is a typical use of pkeys: the data to protect is
mapped with a given pkey P, and the pkey register is initially
configured to grant read-only access to P. Where the protected data
needs to be written to, the pkey register is temporarily switched to
grant write access to P on the current CPU.
The key fact this approach relies on is that the target data is
only written to via a limited and well-defined API. This makes it
possible to explicitly switch the pkey register where needed, without
introducing excessively invasive changes, and only for a small amount of
trusted code.
Page tables are chosen as an initial target because of their especially
critical nature - a single write may result in arbitrary pages becoming
accessible to any context (including userspace). In order to keep the
series digestible for reviewers, this version focuses on functionality
rather than performance, making it most suitable as a debug feature. The
key trade-off is the requirement to PTE-map the linear map - see section
"Protected page table allocation" for details.
This series has similarities with the "PKS write protected page tables"
series posted by Rick Edgecombe a few years ago [1] but it is not
specific to x86/PKS - the approach is meant to be generic.
This proposal (as of RFC v5) was presented at Linux Security Summit
Europe 2025 [2].
[Table of contents]
* kpkeys
- pkey register management
* kpkeys_hardened_pgtables
- Protected page table allocation
- kpkeys context switching
- Performance
- Limitations
* This series
- Branches
* Threat model
* Further use-cases
* Open questions
kpkeys
======
The use of pkeys involves two separate mechanisms: assigning a pkey to
pages, and defining the pkeys -> permissions mapping via the pkey
register. This is implemented through the following interface:
- Pages are assigned a pkey in the linear map using set_memory_pkey().
This is sufficient for this series, but it is also plausible for
higher-level allocators to support marking allocations with a given
pkey.
- The pkey register is configured based on a *kpkeys context*. kpkeys
contexts are represented as simple integers that correspond to a given
configuration, for instance:
KPKEYS_CTX_DEFAULT:
RW access to KPKEYS_PKEY_DEFAULT
RO access to any other KPKEYS_PKEY_*
KPKEYS_CTX_<FEAT>:
RW access to KPKEYS_PKEY_DEFAULT
RW access to KPKEYS_PKEY_<FEAT>
RO access to any other KPKEYS_PKEY_*
Only pkeys that are managed by the kpkeys framework are impacted;
permissions for other pkeys are left unchanged (this allows for other
schemes using pkeys to be used in parallel, and arch-specific use of
certain pkeys).
The kpkeys context is changed by calling kpkeys_set_context(), setting
the pkey register accordingly and returning the original value. A
subsequent call to kpkeys_restore_pkey_reg() restores the kpkeys
context. The numeric value of KPKEYS_CTX_* (kpkeys context) is purely
symbolic and thus generic, however each architecture is free to define
KPKEYS_PKEY_* (pkey value).
pkey register management
------------------------
The kpkeys model relies on the kernel pkey register being set to a
specific value for the duration of a relatively small section of code,
and otherwise to the default value. Two aspects should be considered:
1. Exception entry/return
2. Thread context-switch
Ideally, an implementation would switch the pkey register in both
cases: 1. reset it to a fixed state on exception entry, and 2.
context-switch it along with the other thread registers.
In this series, the arm64 implementation only performs 2. i.e.
context-switching POR_EL1 per-thread. This ensures that each thread's
pkey register is preserved no matter how a thread is rescheduled
(voluntarily or not).
Resetting POR_EL1 on exception entry proved problematic for the
kpkeys_hardened_pgtables feature with the lazy MMU optimisation (see
next section), because page table setters may be used immediately when a
thread resumes after being rescheduled on irqexit, before exception
return. If POR_EL1 is reset on exception entry, we may end up in an
inconsistent state where lazy MMU mode may be enabled but POR_EL1 does
not allow writing to page tables until exception return. For more
details, see [3].
Having the entire exception handling inherit the interrupted POR_EL1 is
not ideal as it increases the amount of code run with a potentially
privileged kpkeys state. It may be possible to pause the lazy MMU mode
to avoid the issue described above, but it isn't clear whether this is
a general enough solution.
An important assumption is that all kpkeys contexts allow RW access to the
default pkey (0). Context-switching and saving/restoring POR_EL1 on
exception entry/return becomes significantly more complex otherwise.
kpkeys_hardened_pgtables
========================
The kpkeys_hardened_pgtables feature uses the interface above to make
the (kernel and user) page tables read-only by default, enabling write
access only in helpers such as set_pte().
Protected page table allocation
-------------------------------
Kernel page tables are required as soon as the MMU is turned on, i.e.
extremely early. Different allocators are therefore used to obtain
page table pages (PTPs), depending on where we are in the boot sequence.
In chronological order:
1. Static pools (in the kernel image itself). Typically used to map the
kernel image. By definition these pools have a fixed size.
2. memblock. Mainly used to create the linear map and sparse-vmemmap
page tables.
3. Buddy allocator, via pagetable_alloc(). Used for everything else,
including user page tables, once available.
No matter which allocator is used, this feature relies on the linear map
being modified to map PTPs with KPKEYS_PKEY_PGTABLES. If large block
mappings are used to create the linear map, blocks may need to be split
when setting the pkey for individual PTPs, which in turn requires a new
PTP to be allocated. RFC v6 introduced a PTP allocator that both handles
splitting and minimises it. This fairly complex component was removed in
RFC v7 so that reviewers can focus on the core logic of the feature,
which is otherwise fairly straightforward.
The present series therefore requires the linear map to be fully
PTE-mapped, so that PTPs can be individually protected without worrying
about block splitting.
The different sources of PTPs listed above are protected as follows:
1. Static pools: an arch hook is introduced to set the pkey of those
PTPs once the linear map is set up.
2. Early page tables: a very simple memblock-based allocator is
introduced. The linear map may not be ready yet when these PTPs are
allocated, so this allocator tracks all allocated pages so that their
pkey can be set later.
3. Regular page tables: directly obtained from the buddy allocator and
their pkey set right away.
All this infrastructure is generic. This series only includes the
required plumbing for arm64, but it is meant to be plumbed into x86 in a
similar manner.
kpkeys context switching
------------------------
Page tables are only supposed to be written via a fairly small set of
helpers, so only a few switches to a privileged kpkeys context need to be
introduced.
However, a naive implementation is very inefficient when many PTEs are
changed at once, as the kpkeys context is switched twice for every PTE.
On arm64, this means two barriers (ISBs) per PTE. This is optimised by
making use of the lazy MMU mode to batch those switches: 1. switch to
KPKEYS_CTX_PGTABLES on arch_enter_lazy_mmu_mode(), 2. skip any kpkeys
switch while in that section, and 3. restore the kpkeys context on
arch_leave_lazy_mmu_mode(). When that last function already issues an
ISB (when updating kernel page tables), we get a further optimisation as
we can skip the ISB when restoring the kpkeys context.
The lazy MMU mode may be enabled/disabled in a nested manner. This is
handled thanks to [5] - if nesting occurs, only the outer section will
switch kpkeys context.
Performance
-----------
No arm64 hardware currently implements POE. Benchmarking has however
been performed on a mock implementation, which should be fairly
representative. On a "real-world" and fork-heavy workload like
kernbench, the estimated overhead is around 2% in system time, while the
real time overhead is negligible. Microbenchmarks focused on
creating/destroying mappings yield overheads up to 15%, but typically
much smaller once batching kicks in (when modifying many PTEs at once).
For more details, see this section in RFC v5 and v6 (linked in the
changelog).
This series
===========
The series is composed of two parts:
- The kpkeys framework (patch 1-9). The main API is introduced in
<linux/kpkeys.h>, and it is implemented on arm64 using the POE
(Permission Overlay Extension) feature.
- The kpkeys_hardened_pgtables feature (patch 10-24). <linux/kpkeys.h>
is extended with an API to protect page table pages and a guard object
to switch kpkeys context accordingly, both gated on a static key. This
is then used in generic and arm64 pgtable handling code as needed.
Finally a simple KUnit-based test suite is added to demonstrate the
page table protection.
Branches
--------
To make reviewing and testing easier, this series is available here:
https://gitlab.arm.com/linux-arm/linux-kb
The following branches are available:
- kpkeys/rfc-v8 - this series, as posted
Threat model
============
The proposed scheme aims at mitigating data-only attacks (e.g.
use-after-free/cross-cache attacks). In other words, it is assumed that
control flow is not corrupted, and that the attacker does not achieve
arbitrary code execution. Nothing prevents the pkey register from being
set to its most permissive state - the assumption is that the register
is only modified on legitimate code paths.
A few related notes:
- Functions that set the pkey register are all implemented inline.
Besides performance considerations, this is meant to avoid creating
a function that can be used as a straightforward gadget to set the
pkey register to an arbitrary value.
- kpkeys_set_context() only accepts a compile-time constant as argument,
as a variable could be manipulated by an attacker. This could be
relaxed but it seems unlikely that a variable kpkeys context would be
needed in practice.
Further use-cases
=================
It should be possible to harden various targets using kpkeys, including:
- struct cred - proposal posted as a separate series [6]
- eBPF programs (preventing direct W/X to core kernel code/data) -
work in progress [7]
- fixmap
- SELinux state (e.g. struct selinux_state::initialized)
... and many others.
kpkeys could also be used to strengthen the confidentiality of secret
data by making it completely inaccessible by default, and granting
read-only or read-write access as needed. This requires such data to be
rarely accessed (or via a limited interface only). One example on arm64
is the pointer authentication keys in thread_struct, whose leakage to
userspace would lead to pointer authentication being easily defeated.
Open questions
==============
A few aspects in this RFC that are debatable and/or worth discussing:
- There is currently no restriction on how kpkeys contexts map to pkeys
permissions. A typical approach is to allocate one pkey per context and
make it writable in that context only. As the number of contexts
increases, we may however run out of pkeys, especially on arm64 (just
8 pkeys with POE). Depending on the use-cases, it may be acceptable to
use the same pkey for the data associated to multiple contexts.
- kpkeys_set_context() and kpkeys_restore_pkey_reg() are not symmetric:
the former takes a kpkeys context and returns a pkey register value,
to be consumed by the latter. It would be more intuitive to manipulate
kpkeys contexts only. However this assumes that there is a 1:1 mapping
between kpkeys contexts and pkey register values, while in principle
the mapping is 1:n (certain pkeys may be used outside the kpkeys
framework).
Any comment or feedback is highly appreciated, be it on the high-level
approach or implementation choices!
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
Changelog
RFC v7..v8:
[Changes below are addressing issues reported by Sashiko [13]]
- Patch 3: do not enable overlays for user memory types in POR_EL1 -
this breaks the atomic futex uaccess operations. Added some
comment and test in a separate series [14] to clarify this.
- Patch 5: fix return type of arch_kpkeys_set_context()
- Patch 7: reset POR_EL1 to POR_EL1_INIT when cloning a thread. The
pkey register value is tied to the context of the original thread
(e.g. being in lazy MMU mode) and should not be inherited by the
new thread in the general case.
- Patch 11: make ARCH_HAS_KPKEYS_HARDENED_PGTABLES explicitly depend
on ARCH_HAS_KPKEYS (not sure why I didn't do that to start with...)
- Patch 14:
* GFP_PGTABLE_KERNEL is not defined on all arch's, use
GFP_KERNEL | __GFP_ZERO instead [reported by kernel CI]
* Amend the arm64 hotplug code to use pagetable_free(), ensuring
that the pkey is reset when vmemmap page tables are freed (this
may belong to a separate patch, surprisingly x86 already uses
pagetable_free() for this purpose)
* Fix new caller of vmemmap_alloc_block_zero(), call memset()
directly since that's the only caller left [also reported by
kernel CI]
* Added a note to the commit message explaining why
__GFP_RETRY_MAYFAIL is not used
- Patch 21: reduced scope of guard in __ptep_set_access_flags_anysz()
- Patch 24: write to P4D/PUD if not folded in write_direct_map_pggtables()
- Other minor improvements
RFC v7: https://lore.kernel.org/all/20260505-kpkeys-v7-0-20c0bdd97197@arm.com/
RFC v6..v7:
- Rebased on v7.1-rc2
- Removed large block mapping support to ease reviewing and focus on the
core changes. [David Hildenbrand] Accordingly, patched 19 was modified
to force PTE mappings if kpkeys_hardened_pgtables is enabled.
- Added patch 14 to protect the sparse-vmemmap page tables.
- Dropped patch "arm64: Reset POR_EL1 on exception entry" as it may
cause a crash when a thread with lazy MMU mode enabled is resumed on
the irqexit path. See [3] and section "pkey register management" for
details. The POR_EL1_INIT bits were squashed into patch 5 (they are
still needed for patch 8).
- Renamed "kpkeys level" to "kpkeys context" as there is no longer a
notion of ordering - each "context" may be given access to any set of
pkeys. [David H]
- Re-organised *_enabled() functions [partially suggested by David H].
The interface is now fully generic and defined in <linux/kpkeys.h>:
- kpkeys_enabled() -> arch_supports_kpkeys()
- kpkeys_hardened_pgtables_enabled() -> static key
- kpkeys_hardened_pgtables_early_enabled() -> arch_supports_kpkeys_early()
Each arch then implements (without #ifdef'ing required):
- arch_supports_kpkeys()
- arch_supports_kpkeys_early()
See patch 1, 5, 11, 16 for details.
- Patch 2: remove KASAN tag when checking if the address falls in the
linear map [reported by Yeoreum Yun, thanks!]
- Patch 11: export symbol for the lazy MMU KUnit tests (reported by
build CI)
- Patch 24: added test for the sparse-vmemmap page tables
RFC v6: https://lore.kernel.org/linux-hardening/20260227175518.3728055-1-kevin.brodsky@arm.com/
RFC v5..v6:
- Rebased on v7.0-rc1 (includes support for splitting large block
mappings with BBML2-noabort [4] and nested lazy MMU sections [5])
- Completely new approach for allocating protected page tables, see
section "Protected page table allocation". Patch 11-26 are mostly new.
- Patch 6: check arch_kpkeys_enabled() to make sure that page table
attributes are still changed when using the mock implementation
- Patch 9: new patch (thank you Yeoreum!)
- Patch 28: minor changes following the merging of the nested lazy MMU
series [5]
- Patch 30:
* Many tests added to provide better coverage of the various page
table allocation paths
* Require that the tests are built-in (many referenced symbols are
not exported)
* Some refactoring to reduce duplication and add logging
RFC v5: https://lore.kernel.org/linux-hardening/20250815085512.2182322-1-kevin.brodsky@arm.com/
RFC v4..v5:
- Rebased on v6.17-rc1.
- Cover letter: re-ran benchmarks on top of v6.17-rc1, made various
small improvements especially to the "Performance" section.
- Patch 18: disable batching while in interrupt, since POR_EL1 is reset
on exception entry, making the TIF_LAZY_MMU flag meaningless. This
fixes a crash that may occur when a page table page is freed while in
interrupt context.
- Patch 17: ensure that the target kernel address is actually
PTE-mapped. Certain mappings (e.g. code) may be PMD-mapped instead -
this explains why the change made in v4 was required.
RFC v4: https://lore.kernel.org/linux-mm/20250411091631.954228-1-kevin.brodsky@arm.com/
RFC v3..v4:
- Added appropriate handling of the arm64 pkey register (POR_EL1):
context-switching between threads and resetting on exception entry
(patch 7 and 8). See section "pkey register management" above for more
details. A new POR_EL1_INIT macro is introduced to make the default
value available to assembly (where POR_EL1 is reset on exception
entry); it is updated in each patch allocating new keys.
- Added patch 18 making use of the lazy_mmu mode to batch switches to
KPKEYS_LVL_PGTABLES - just once per lazy_mmu section rather than on
every pgtable write. See section "Performance" for details.
- Rebased on top of [8]. No direct impact on the patches, but it ensures that
the ctor/dtor is always called for kernel pgtables. This is an
important fix as kernel PTEs allocated after boot were not protected
by kpkeys_hardened_pgtables in v3 - a new test was added to patch 17
to ensure that pgtables created by vmalloc are protected too.
- Rebased on top of [9]. The batching of kpkeys level switches in patch
18 relies on the last patch in [9].
- Moved kpkeys guard definitions out of <linux/kpkeys.h> and to a relevant
header for each subsystem (e.g. <asm/pgtable.h> for the
kpkeys_hardened_pgtables guard).
- Patch 1,5: marked kpkeys_{set_level,restore_pkey_reg} as
__always_inline to ensure that no callable gadget is created.
[Maxwell Bland's suggestion]
- Patch 5: added helper __kpkeys_set_pkey_reg_nosync().
- Patch 10: marked kernel_pgtables_set_pkey() and related helpers as
__init. [Linus Walleij's suggestion]
- Patch 11: added helper kpkeys_hardened_pgtables_enabled(), renamed the
static key to kpkeys_hardened_pgtables_key.
- Patch 17: followed the KUnit conventions more closely. [Kees Cook's
suggestion]
- Patch 17: changed the address used in the write_linear_map_pte()
test. It seems that the PTEs that map some functions are allocated in
ZONE_DMA and read-only (unclear why exactly). This doesn't seem to
occur for global variables.
- Various minor fixes/improvements.
- Rebased on v6.15-rc1. This includes [10], which renames a few POE
symbols: s/POE_RXW/POE_RWX/ and
s/por_set_pkey_perms/por_elx_set_pkey_perms/
RFC v3: https://lore.kernel.org/linux-hardening/20250203101839.1223008-1-kevin.brodsky@arm.com/
RFC v2..v3:
- Patch 1: kpkeys_set_level() may now return KPKEYS_PKEY_REG_INVAL to indicate
that the pkey register wasn't written to, and as a result that
kpkeys_restore_pkey_reg() should do nothing. This simplifies the conditional
guard macro and also allows architectures to skip writes to the pkey
register if the target value is the same as the current one.
- Patch 1: introduced additional KPKEYS_GUARD* macros to cover more use-cases
and reduce duplication.
- Patch 6: reject pkey value above arch_max_pkey().
- Patch 13: added missing guard(kpkeys_hardened_pgtables) in
__clear_young_dirty_pte().
- Rebased on v6.14-rc1.
RFC v2: https://lore.kernel.org/linux-hardening/20250108103250.3188419-1-kevin.brodsky@arm.com/
RFC v1..v2:
- A new approach is used to set the pkey of page table pages. Thanks to
Qi Zheng's and my own series [11][12], pagetable_*_ctor is
systematically called when a PTP is allocated at any level (PTE to
PGD), and pagetable_*_dtor when it is freed, on all architectures.
Patch 11 makes use of this to call kpkeys_{,un}protect_pgtable_memory
from the common ctor/dtor helper. The arm64 patches from v1 (patch 12
and 13) are dropped as they are no longer needed. Patch 10 is
introduced to allow pagetable_*_ctor to fail at all levels, since
kpkeys_protect_pgtable_memory may itself fail.
[Original suggestion by Peter Zijlstra]
- Changed the prototype of kpkeys_{,un}protect_pgtable_memory in patch 9
to take a struct folio * for more convenience, and implemented them
out-of-line to avoid a circular dependency with <linux/mm.h>.
- Rebased on next-20250107, which includes [11] and [12].
- Added locking in patch 8. [Peter Zijlstra's suggestion]
RFC v1: https://lore.kernel.org/linux-hardening/20241206101110.1646108-1-kevin.brodsky@arm.com/
---
References
[1] https://lore.kernel.org/all/20210830235927.6443-1-rick.p.edgecombe@intel.com/
[2] https://lsseu2025.sched.com/event/25GEE/kernel-hardening-with-protection-keys-kevin-brodsky-arm
[3] https://lore.kernel.org/linux-hardening/7dc9485d-a822-494d-9384-4a973c782c11@arm.com/
[4] https://lore.kernel.org/all/20250917190323.3828347-1-yang@os.amperecomputing.com/
[5] https://lore.kernel.org/all/20251215150323.2218608-1-kevin.brodsky@arm.com/
[6] https://lore.kernel.org/linux-mm/20250815090000.2182450-1-kevin.brodsky@arm.com/
[7] https://lore.kernel.org/all/aY3+Raf8eZqipCd6@e129823.arm.com/
[8] https://lore.kernel.org/linux-mm/20250408095222.860601-1-kevin.brodsky@arm.com/
[9] https://lore.kernel.org/linux-mm/20250304150444.3788920-1-ryan.roberts@arm.com/
[10] https://lore.kernel.org/linux-arm-kernel/20250219164029.2309119-1-kevin.brodsky@arm.com/
[11] https://lore.kernel.org/linux-mm/cover.1736317725.git.zhengqi.arch@bytedance.com/
[12] https://lore.kernel.org/linux-mm/20250103184415.2744423-1-kevin.brodsky@arm.com/
[13] https://sashiko.dev/#/patchset/20260505-kpkeys-v7-0-20c0bdd97197%40arm.com
[14] https://lore.kernel.org/all/20260521-poe_futex-v1-0-1da286b8f9b2@arm.com/
---
To: linux-hardening@vger.kernel.org
Cc: Andrew Morton <akpm@linux-foundation.org>
Cc: Andy Lutomirski <luto@kernel.org>
Cc: Catalin Marinas <catalin.marinas@arm.com>
Cc: Dave Hansen <dave.hansen@linux.intel.com>
Cc: David Hildenbrand (Arm) <david@kernel.org>
Cc: Ira Weiny <ira.weiny@intel.com>
Cc: Jann Horn <jannh@google.com>
Cc: Jeff Xu <jeffxu@chromium.org>
Cc: Joey Gouly <joey.gouly@arm.com>
Cc: Kees Cook <kees@kernel.org>
Cc: Linus Walleij <linusw@kernel.org>
Cc: Lorenzo Stoakes <lorenzo.stoakes@oracle.com>
Cc: Marc Zyngier <maz@kernel.org>
Cc: Mark Brown <broonie@kernel.org>
Cc: Matthew Wilcox <willy@infradead.org>
Cc: Maxwell Bland <mbland@motorola.com>
Cc: "Mike Rapoport (IBM)" <rppt@kernel.org>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Pierre Langlois <pierre.langlois@arm.com>
Cc: Quentin Perret <qperret@google.com>
Cc: Rick Edgecombe <rick.p.edgecombe@intel.com>
Cc: Ryan Roberts <ryan.roberts@arm.com>
Cc: Thomas Gleixner <tglx@linutronix.de>
Cc: Vlastimil Babka <vbabka@kernel.org>
Cc: Will Deacon <will@kernel.org>
Cc: Yang Shi <yang@os.amperecomputing.com>
Cc: Yeoreum Yun <yeoreum.yun@arm.com>
Cc: linux-arm-kernel@lists.infradead.org
Cc: linux-mm@kvack.org
Cc: x86@kernel.org
---
Kevin Brodsky (23):
mm: Introduce kpkeys
set_memory: Introduce set_memory_pkey() stub
arm64: mm: Enable overlays for all EL1 indirect permissions
arm64: Introduce por_elx_set_pkey_perms() helper
arm64: Implement asm/kpkeys.h using POE
arm64: set_memory: Implement set_memory_pkey()
arm64: Context-switch POR_EL1
arm64: Enable kpkeys
memblock: Move INIT_MEMBLOCK_* macros to header
mm: kpkeys: Introduce kpkeys_hardened_pgtables feature
mm: kpkeys: Protect regular page tables
mm: kpkeys: Introduce early page table allocator
mm: kpkeys: Protect vmemmap page tables
mm: kpkeys: Introduce hook for protecting static page tables
arm64: kpkeys: Implement arch_supports_kpkeys_early()
arm64: kpkeys: Support KPKEYS_CTX_PGTABLES
arm64: kpkeys: Ensure the linear map can be modified
arm64: kpkeys: Protect early page tables
arm64: kpkeys: Protect init_pg_dir
arm64: kpkeys: Guard page table writes
arm64: kpkeys: Batch KPKEYS_CTX_PGTABLES switches
arm64: kpkeys: Enable kpkeys_hardened_pgtables support
mm: Add basic tests for kpkeys_hardened_pgtables
Yeoreum Yun (1):
arm64: Initialize POR_EL1 register on cpu_resume()
arch/arm64/Kconfig | 2 +
arch/arm64/include/asm/cpufeature.h | 12 ++
arch/arm64/include/asm/kpkeys.h | 76 ++++++++++++
arch/arm64/include/asm/pgtable-prot.h | 8 +-
arch/arm64/include/asm/pgtable.h | 66 +++++++++-
arch/arm64/include/asm/por.h | 11 ++
arch/arm64/include/asm/processor.h | 2 +
arch/arm64/include/asm/set_memory.h | 4 +
arch/arm64/kernel/cpufeature.c | 5 +-
arch/arm64/kernel/process.c | 10 ++
arch/arm64/kernel/sleep.S | 12 ++
arch/arm64/mm/fault.c | 17 +--
arch/arm64/mm/init.c | 1 +
arch/arm64/mm/mmu.c | 50 +++++---
arch/arm64/mm/pageattr.c | 29 ++++-
include/asm-generic/kpkeys.h | 21 ++++
include/linux/kpkeys.h | 181 +++++++++++++++++++++++++++
include/linux/memblock.h | 11 ++
include/linux/mm.h | 14 ++-
include/linux/set_memory.h | 7 ++
mm/Kconfig | 5 +
mm/Makefile | 2 +
mm/kpkeys_hardened_pgtables.c | 179 ++++++++++++++++++++++++++
mm/memblock.c | 11 --
mm/sparse-vmemmap.c | 33 +++--
mm/tests/kpkeys_hardened_pgtables_kunit.c | 200 ++++++++++++++++++++++++++++++
security/Kconfig.hardening | 24 ++++
27 files changed, 937 insertions(+), 56 deletions(-)
---
base-commit: e7ae89a0c97ce2b68b0983cd01eda67cf373517d
change-id: 20260428-kpkeys-e165645122c1
^ permalink raw reply
* [PATCH RFC v8 01/24] mm: Introduce kpkeys
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
kpkeys is a simple framework to enable the use of protection keys
(pkeys) to harden the kernel itself. This patch introduces the basic
API in <linux/kpkeys.h>: a couple of functions to set and restore
the pkey register and macros to define guard objects.
kpkeys introduces a new concept on top of pkeys: the kpkeys context.
Each context is associated to a set of permissions for the pkeys
managed by the kpkeys framework. kpkeys_set_context(ctx) sets those
permissions according to ctx, and returns the original pkey
register, to be later restored by kpkeys_restore_pkey_reg(). To
start with, only KPKEYS_CTX_DEFAULT is available, which is meant to
grant RW access to KPKEYS_PKEY_DEFAULT (i.e. all memory since this
is the only available pkey for now).
Because each architecture implementing pkeys uses a different
representation for the pkey register, and may reserve certain pkeys
for specific uses, support for kpkeys must be explicitly indicated
by selecting ARCH_HAS_KPKEYS and defining the following functions in
<asm/kpkeys.h>, in addition to the macros provided in
<asm-generic/kpkeys.h>:
- arch_kpkeys_set_context()
- arch_kpkeys_restore_pkey_reg()
- arch_supports_kpkeys()
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/asm-generic/kpkeys.h | 17 ++++++
include/linux/kpkeys.h | 122 +++++++++++++++++++++++++++++++++++++++++++
mm/Kconfig | 2 +
3 files changed, 141 insertions(+)
diff --git a/include/asm-generic/kpkeys.h b/include/asm-generic/kpkeys.h
new file mode 100644
index 000000000000..ab819f157d6a
--- /dev/null
+++ b/include/asm-generic/kpkeys.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __ASM_GENERIC_KPKEYS_H
+#define __ASM_GENERIC_KPKEYS_H
+
+#ifndef KPKEYS_PKEY_DEFAULT
+#define KPKEYS_PKEY_DEFAULT 0
+#endif
+
+/*
+ * Represents a pkey register value that cannot be used, typically disabling
+ * access to all keys.
+ */
+#ifndef KPKEYS_PKEY_REG_INVAL
+#define KPKEYS_PKEY_REG_INVAL 0
+#endif
+
+#endif /* __ASM_GENERIC_KPKEYS_H */
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
new file mode 100644
index 000000000000..0ce0db80b4ee
--- /dev/null
+++ b/include/linux/kpkeys.h
@@ -0,0 +1,122 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _LINUX_KPKEYS_H
+#define _LINUX_KPKEYS_H
+
+#include <linux/bug.h>
+#include <linux/cleanup.h>
+
+#define KPKEYS_CTX_DEFAULT 0
+
+#define KPKEYS_CTX_MIN KPKEYS_CTX_DEFAULT
+#define KPKEYS_CTX_MAX KPKEYS_CTX_DEFAULT
+
+/*
+ * ... is used to discard extra arguments - this allows users of this macro
+ * to have set_arg default to void.
+ */
+#define __KPKEYS_GUARD(name, set_context, restore_pkey_reg, set_arg, ...) \
+ __DEFINE_CLASS_IS_CONDITIONAL(name, false); \
+ DEFINE_CLASS(name, u64, \
+ restore_pkey_reg, set_context, set_arg); \
+ static inline void *class_##name##_lock_ptr(u64 *_T) \
+ { return _T; }
+
+/**
+ * KPKEYS_GUARD_NOOP() - define a guard type that does nothing
+ * @name: the name of the guard type
+ * @cond_arg: an argument specification (optional)
+ *
+ * Define a guard type that does nothing, useful to match a real guard type
+ * that is defined under an #ifdef. @cond_arg may optionally be passed to match
+ * a guard defined using KPKEYS_GUARD_COND().
+ */
+#define KPKEYS_GUARD_NOOP(name, ...) \
+ __KPKEYS_GUARD(name, 0, (void)_T, ##__VA_ARGS__, void)
+
+#ifdef CONFIG_ARCH_HAS_KPKEYS
+
+#include <asm/kpkeys.h>
+
+/**
+ * KPKEYS_GUARD_COND() - define a guard type that conditionally switches to
+ * a given kpkeys context
+ * @name: the name of the guard type
+ * @ctx: the kpkeys context to switch to
+ * @cond: an expression that is evaluated as condition
+ * @cond_arg: an argument specification for the condition (optional)
+ *
+ * Define a guard type that switches to @ctx if @cond evaluates to true,
+ * and does nothing otherwise. @cond_arg may be specified to give access to a
+ * caller-defined argument to @cond.
+ */
+#define KPKEYS_GUARD_COND(name, ctx, cond, ...) \
+ __KPKEYS_GUARD(name, \
+ (cond) ? kpkeys_set_context(ctx) \
+ : KPKEYS_PKEY_REG_INVAL, \
+ kpkeys_restore_pkey_reg(_T), \
+ ##__VA_ARGS__, void)
+
+/**
+ * KPKEYS_GUARD() - define a guard type that switches to a given kpkeys context
+ * if kpkeys are enabled
+ * @name: the name of the guard type
+ * @ctx: the kpkeys context to switch to
+ *
+ * Define a guard type that switches to @ctx if the system supports kpkeys.
+ */
+#define KPKEYS_GUARD(name, ctx) \
+ KPKEYS_GUARD_COND(name, ctx, kpkeys_enabled())
+
+/**
+ * kpkeys_set_context() - switch kpkeys context
+ * @ctx: the context to switch to
+ *
+ * Switches to specified kpkeys context. @ctx must be a compile-time
+ * constant. The arch-specific pkey register will be updated accordingly, and
+ * the original value returned.
+ *
+ * Return: the original pkey register value if the register was written to, or
+ * KPKEYS_PKEY_REG_INVAL otherwise (no write to the register was
+ * required).
+ */
+static __always_inline u64 kpkeys_set_context(int ctx)
+{
+ BUILD_BUG_ON_MSG(!__builtin_constant_p(ctx),
+ "kpkeys_set_context() only takes constant values");
+ BUILD_BUG_ON_MSG(ctx < KPKEYS_CTX_MIN || ctx > KPKEYS_CTX_MAX,
+ "Invalid value passed to kpkeys_set_context()");
+
+ return arch_kpkeys_set_context(ctx);
+}
+
+/**
+ * kpkeys_restore_pkey_reg() - restores a pkey register value
+ * @pkey_reg: the pkey register value to restore
+ *
+ * This function is meant to be passed the value returned by
+ * kpkeys_set_context(), in order to restore the pkey register to its original
+ * value (thus restoring the original kpkeys context).
+ */
+static __always_inline void kpkeys_restore_pkey_reg(u64 pkey_reg)
+{
+ if (pkey_reg != KPKEYS_PKEY_REG_INVAL)
+ arch_kpkeys_restore_pkey_reg(pkey_reg);
+}
+
+static inline bool kpkeys_enabled(void)
+{
+ return arch_supports_kpkeys();
+}
+
+#else /* CONFIG_ARCH_HAS_KPKEYS */
+
+#include <asm-generic/kpkeys.h>
+
+static inline bool kpkeys_enabled(void)
+{
+ return false;
+}
+
+#endif /* CONFIG_ARCH_HAS_KPKEYS */
+
+#endif /* _LINUX_KPKEYS_H */
diff --git a/mm/Kconfig b/mm/Kconfig
index e8bf1e9e6ad9..819fb0d7b7bd 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -1242,6 +1242,8 @@ config ARCH_USES_HIGH_VMA_FLAGS
bool
config ARCH_HAS_PKEYS
bool
+config ARCH_HAS_KPKEYS
+ bool
config ARCH_USES_PG_ARCH_2
bool
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 02/24] set_memory: Introduce set_memory_pkey() stub
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
Introduce a new function, set_memory_pkey(), which sets the
protection key (pkey) of pages in the specified linear mapping
range. Architectures implementing kernel pkeys (kpkeys) must
provide a suitable implementation; an empty stub is added as
fallback.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/set_memory.h | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/include/linux/set_memory.h b/include/linux/set_memory.h
index 3030d9245f5a..7b3a8bfde3c6 100644
--- a/include/linux/set_memory.h
+++ b/include/linux/set_memory.h
@@ -84,4 +84,11 @@ static inline int set_memory_decrypted(unsigned long addr, int numpages)
}
#endif /* CONFIG_ARCH_HAS_MEM_ENCRYPT */
+#ifndef CONFIG_ARCH_HAS_KPKEYS
+static inline int set_memory_pkey(unsigned long addr, int numpages, int pkey)
+{
+ return 0;
+}
+#endif
+
#endif /* _LINUX_SET_MEMORY_H_ */
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 03/24] arm64: mm: Enable overlays for all EL1 indirect permissions
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
In preparation of using POE inside the kernel, enable "Overlay
applied" for kernel memory types in PIR_EL1. This ensures that the
permissions set in POR_EL1 affect all kernel mappings.
User memory types must be left untouched (overlays not applied)
because any privileged access to user memory (e.g. futex atomic
without FEAT_LSUI) would then be mistakenly checked against POR_EL1.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/pgtable-prot.h | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/include/asm/pgtable-prot.h b/arch/arm64/include/asm/pgtable-prot.h
index 212ce1b02e15..d4d45ab86a5a 100644
--- a/arch/arm64/include/asm/pgtable-prot.h
+++ b/arch/arm64/include/asm/pgtable-prot.h
@@ -183,9 +183,9 @@ static inline bool __pure lpa2_is_enabled(void)
PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_SHARED_EXEC), PIE_RW) | \
PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_READONLY), PIE_R) | \
PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_SHARED), PIE_RW) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_ROX), PIE_RX) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_EXEC), PIE_RWX) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_RO), PIE_R) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL), PIE_RW))
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_ROX), PIE_RX_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_EXEC), PIE_RWX_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_RO), PIE_R_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL), PIE_RW_O))
#endif /* __ASM_PGTABLE_PROT_H */
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 04/24] arm64: Introduce por_elx_set_pkey_perms() helper
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
Introduce a helper that sets the permissions of a given pkey
(POIndex) in the POR_ELx format, and make use of it in
arch_set_user_pkey_access().
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/por.h | 7 +++++++
arch/arm64/mm/mmu.c | 26 ++++++++++----------------
2 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/arch/arm64/include/asm/por.h b/arch/arm64/include/asm/por.h
index d913d5b529e4..bffb4d2b1246 100644
--- a/arch/arm64/include/asm/por.h
+++ b/arch/arm64/include/asm/por.h
@@ -31,4 +31,11 @@ static inline bool por_elx_allows_exec(u64 por, u8 pkey)
return perm & POE_X;
}
+static inline u64 por_elx_set_pkey_perms(u64 por, u8 pkey, u64 perms)
+{
+ u64 shift = POR_ELx_PERM_SHIFT(pkey);
+
+ return (por & ~(POE_MASK << shift)) | (perms << shift);
+}
+
#endif /* _ASM_ARM64_POR_H */
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index dd85e093ffdb..493310cf0486 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -2339,8 +2339,8 @@ void __cpu_replace_ttbr1(pgd_t *pgdp, bool cnp)
#ifdef CONFIG_ARCH_HAS_PKEYS
int arch_set_user_pkey_access(int pkey, unsigned long init_val)
{
- u64 new_por;
- u64 old_por;
+ u64 new_perms;
+ u64 por;
if (!system_supports_poe())
return -ENOSPC;
@@ -2354,25 +2354,19 @@ int arch_set_user_pkey_access(int pkey, unsigned long init_val)
return -EINVAL;
/* Set the bits we need in POR: */
- new_por = POE_RWX;
+ new_perms = POE_RWX;
if (init_val & PKEY_DISABLE_WRITE)
- new_por &= ~POE_W;
+ new_perms &= ~POE_W;
if (init_val & PKEY_DISABLE_ACCESS)
- new_por &= ~POE_RW;
+ new_perms &= ~POE_RW;
if (init_val & PKEY_DISABLE_READ)
- new_por &= ~POE_R;
+ new_perms &= ~POE_R;
if (init_val & PKEY_DISABLE_EXECUTE)
- new_por &= ~POE_X;
+ new_perms &= ~POE_X;
- /* Shift the bits in to the correct place in POR for pkey: */
- new_por = POR_ELx_PERM_PREP(pkey, new_por);
-
- /* Get old POR and mask off any old bits in place: */
- old_por = read_sysreg_s(SYS_POR_EL0);
- old_por &= ~(POE_MASK << POR_ELx_PERM_SHIFT(pkey));
-
- /* Write old part along with new part: */
- write_sysreg_s(old_por | new_por, SYS_POR_EL0);
+ por = read_sysreg_s(SYS_POR_EL0);
+ por = por_elx_set_pkey_perms(por, pkey, new_perms);
+ write_sysreg_s(por, SYS_POR_EL0);
return 0;
}
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 05/24] arm64: Implement asm/kpkeys.h using POE
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
Implement the kpkeys interface if CONFIG_ARM64_POE is enabled.
The permissions for KPKEYS_PKEY_DEFAULT (pkey 0) are set to RWX as
this pkey is also used for code mappings.
To allow <asm/kpkeys.h> to be included from assembly, also add
appropriate #ifdef's to <asm/por.h>.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/kpkeys.h | 59 +++++++++++++++++++++++++++++++++++++++++
arch/arm64/include/asm/por.h | 4 +++
2 files changed, 63 insertions(+)
diff --git a/arch/arm64/include/asm/kpkeys.h b/arch/arm64/include/asm/kpkeys.h
new file mode 100644
index 000000000000..4dbfeb3dfcfe
--- /dev/null
+++ b/arch/arm64/include/asm/kpkeys.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __ASM_KPKEYS_H
+#define __ASM_KPKEYS_H
+
+#include <asm/barrier.h>
+#include <asm/cpufeature.h>
+#include <asm/por.h>
+
+#include <asm-generic/kpkeys.h>
+
+/*
+ * Equivalent to por_set_kpkeys_context(0, KPKEYS_CTX_DEFAULT), but can also be
+ * used in assembly.
+ */
+#define POR_EL1_INIT POR_ELx_PERM_PREP(KPKEYS_PKEY_DEFAULT, POE_RWX)
+
+#ifndef __ASSEMBLY__
+
+static inline bool arch_supports_kpkeys(void)
+{
+ return system_supports_poe();
+}
+
+#ifdef CONFIG_ARM64_POE
+
+static inline u64 por_set_kpkeys_context(u64 por, int ctx)
+{
+ por = por_elx_set_pkey_perms(por, KPKEYS_PKEY_DEFAULT, POE_RWX);
+
+ return por;
+}
+
+static __always_inline void __kpkeys_set_pkey_reg_nosync(u64 pkey_reg)
+{
+ write_sysreg_s(pkey_reg, SYS_POR_EL1);
+}
+
+static __always_inline u64 arch_kpkeys_set_context(int ctx)
+{
+ u64 prev_por = read_sysreg_s(SYS_POR_EL1);
+ u64 new_por = por_set_kpkeys_context(prev_por, ctx);
+
+ __kpkeys_set_pkey_reg_nosync(new_por);
+ isb();
+
+ return prev_por;
+}
+
+static __always_inline void arch_kpkeys_restore_pkey_reg(u64 pkey_reg)
+{
+ __kpkeys_set_pkey_reg_nosync(pkey_reg);
+ isb();
+}
+
+#endif /* CONFIG_ARM64_POE */
+
+#endif /* __ASSEMBLY__ */
+
+#endif /* __ASM_KPKEYS_H */
diff --git a/arch/arm64/include/asm/por.h b/arch/arm64/include/asm/por.h
index bffb4d2b1246..58dce4b8021b 100644
--- a/arch/arm64/include/asm/por.h
+++ b/arch/arm64/include/asm/por.h
@@ -10,6 +10,8 @@
#define POR_EL0_INIT POR_ELx_PERM_PREP(0, POE_RWX)
+#ifndef __ASSEMBLY__
+
static inline bool por_elx_allows_read(u64 por, u8 pkey)
{
u8 perm = POR_ELx_PERM_GET(pkey, por);
@@ -38,4 +40,6 @@ static inline u64 por_elx_set_pkey_perms(u64 por, u8 pkey, u64 perms)
return (por & ~(POE_MASK << shift)) | (perms << shift);
}
+#endif /* __ASSEMBLY__ */
+
#endif /* _ASM_ARM64_POR_H */
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 06/24] arm64: set_memory: Implement set_memory_pkey()
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
Implement set_memory_pkey() using POE.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/set_memory.h | 4 ++++
arch/arm64/mm/pageattr.c | 26 ++++++++++++++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/arch/arm64/include/asm/set_memory.h b/arch/arm64/include/asm/set_memory.h
index 90f61b17275e..b6cd6de34abf 100644
--- a/arch/arm64/include/asm/set_memory.h
+++ b/arch/arm64/include/asm/set_memory.h
@@ -19,4 +19,8 @@ bool kernel_page_present(struct page *page);
int set_memory_encrypted(unsigned long addr, int numpages);
int set_memory_decrypted(unsigned long addr, int numpages);
+#ifdef CONFIG_ARCH_HAS_KPKEYS
+int set_memory_pkey(unsigned long addr, int numpages, int pkey);
+#endif
+
#endif /* _ASM_ARM64_SET_MEMORY_H */
diff --git a/arch/arm64/mm/pageattr.c b/arch/arm64/mm/pageattr.c
index ce035e1b4eaf..8f3519400bd1 100644
--- a/arch/arm64/mm/pageattr.c
+++ b/arch/arm64/mm/pageattr.c
@@ -9,6 +9,8 @@
#include <linux/sched.h>
#include <linux/vmalloc.h>
#include <linux/pagewalk.h>
+#include <linux/pkeys.h>
+#include <linux/kpkeys.h>
#include <asm/cacheflush.h>
#include <asm/pgtable-prot.h>
@@ -365,6 +367,30 @@ int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid)
return set_memory_valid(addr, nr, valid);
}
+#ifdef CONFIG_ARCH_HAS_KPKEYS
+int set_memory_pkey(unsigned long addr, int numpages, int pkey)
+{
+ unsigned long set_prot = 0;
+
+ if (!kpkeys_enabled())
+ return 0;
+
+ if (!__is_lm_address(kasan_reset_tag((void *)addr)))
+ return -EINVAL;
+
+ if (pkey < 0 || pkey >= arch_max_pkey())
+ return -EINVAL;
+
+ set_prot |= pkey & BIT(0) ? PTE_PO_IDX_0 : 0;
+ set_prot |= pkey & BIT(1) ? PTE_PO_IDX_1 : 0;
+ set_prot |= pkey & BIT(2) ? PTE_PO_IDX_2 : 0;
+
+ return __change_memory_common(addr, PAGE_SIZE * numpages,
+ __pgprot(set_prot),
+ __pgprot(PTE_PO_IDX_MASK));
+}
+#endif
+
#ifdef CONFIG_DEBUG_PAGEALLOC
/*
* This is - apart from the return value - doing the same
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 07/24] arm64: Context-switch POR_EL1
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
POR_EL1 is about to be used by the kpkeys framework, modifying it
for (typically small) sections of code. If an exception occurs
during that window and scheduling occurs, we must ensure that
POR_EL1 is context-switched as needed (saving the old value and
restoring the new one). An ISB is needed to ensure the write takes
effect, so we skip it if the new value is the same as the old, like
for POR_EL0.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/processor.h | 1 +
arch/arm64/kernel/process.c | 10 ++++++++++
2 files changed, 11 insertions(+)
diff --git a/arch/arm64/include/asm/processor.h b/arch/arm64/include/asm/processor.h
index e30c4c8e3a7a..6095322343fc 100644
--- a/arch/arm64/include/asm/processor.h
+++ b/arch/arm64/include/asm/processor.h
@@ -192,6 +192,7 @@ struct thread_struct {
u64 svcr;
u64 tpidr2_el0;
u64 por_el0;
+ u64 por_el1;
#ifdef CONFIG_ARM64_GCS
unsigned int gcs_el0_mode;
unsigned int gcs_el0_locked;
diff --git a/arch/arm64/kernel/process.c b/arch/arm64/kernel/process.c
index 033643cd4e5e..e5a9c210fbde 100644
--- a/arch/arm64/kernel/process.c
+++ b/arch/arm64/kernel/process.c
@@ -51,6 +51,7 @@
#include <asm/exec.h>
#include <asm/fpsimd.h>
#include <asm/gcs.h>
+#include <asm/kpkeys.h>
#include <asm/mmu_context.h>
#include <asm/mpam.h>
#include <asm/mte.h>
@@ -466,6 +467,9 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args)
ptrauth_thread_init_kernel(p);
+ if (system_supports_poe())
+ p->thread.por_el1 = POR_EL1_INIT;
+
if (likely(!args->fn)) {
*childregs = *current_pt_regs();
childregs->regs[0] = 0;
@@ -716,6 +720,12 @@ static void permission_overlay_switch(struct task_struct *next)
* of POR_EL0.
*/
}
+
+ current->thread.por_el1 = read_sysreg_s(SYS_POR_EL1);
+ if (current->thread.por_el1 != next->thread.por_el1) {
+ write_sysreg_s(next->thread.por_el1, SYS_POR_EL1);
+ isb();
+ }
}
/*
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 08/24] arm64: Initialize POR_EL1 register on cpu_resume()
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
From: Yeoreum Yun <yeoreum.yun@arm.com>
The POR_EL1 register is reset to an unknown value after cpu_suspend().
Since POR_EL1 always holds POR_EL1_INIT when entering cpu_suspend(),
initialize POR_EL1 with POR_EL1_INIT before cpu_do_resume().
Signed-off-by: Yeoreum Yun <yeoreum.yun@arm.com>
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/kernel/sleep.S | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/arch/arm64/kernel/sleep.S b/arch/arm64/kernel/sleep.S
index f093cdf71be1..e0a6ad85cd24 100644
--- a/arch/arm64/kernel/sleep.S
+++ b/arch/arm64/kernel/sleep.S
@@ -3,6 +3,7 @@
#include <linux/linkage.h>
#include <asm/asm-offsets.h>
#include <asm/assembler.h>
+#include <asm/kpkeys.h>
#include <asm/smp.h>
.text
@@ -134,6 +135,17 @@ SYM_FUNC_START(_cpu_resume)
/* load sp from context */
ldr x2, [x0, #CPU_CTX_SP]
mov sp, x2
+
+#ifdef CONFIG_ARM64_POE
+alternative_if_not ARM64_HAS_S1POE
+ b .Lskip_por_set
+alternative_else_nop_endif
+ mov_q x2, POR_EL1_INIT
+ msr_s SYS_POR_EL1, x2
+ /* isb can be skipped since cpu_do_resume() will do it. */
+.Lskip_por_set:
+#endif /* CONFIG_ARM64_POE */
+
/*
* cpu_do_resume expects x0 to contain context address pointer
*/
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 09/24] arm64: Enable kpkeys
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
This is the final step to enable kpkeys on arm64. We enable
POE at EL1 by setting TCR2_EL1.POE, and initialise POR_EL1 to the
default value, enabling access to the default pkey/POIndex (0).
An ISB is added so that POE restrictions are enforced immediately.
Having done this, we can now select ARCH_HAS_KPKEYS if ARM64_POE is
enabled.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/Kconfig | 1 +
arch/arm64/kernel/cpufeature.c | 5 ++++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/Kconfig b/arch/arm64/Kconfig
index fe60738e5943..ab06324a50ae 100644
--- a/arch/arm64/Kconfig
+++ b/arch/arm64/Kconfig
@@ -2145,6 +2145,7 @@ config ARM64_POE
def_bool y
select ARCH_USES_HIGH_VMA_FLAGS
select ARCH_HAS_PKEYS
+ select ARCH_HAS_KPKEYS
help
The Permission Overlay Extension is used to implement Memory
Protection Keys. Memory Protection Keys provides a mechanism for
diff --git a/arch/arm64/kernel/cpufeature.c b/arch/arm64/kernel/cpufeature.c
index 6d53bb15cf7b..e032322c0c36 100644
--- a/arch/arm64/kernel/cpufeature.c
+++ b/arch/arm64/kernel/cpufeature.c
@@ -76,6 +76,7 @@
#include <linux/kasan.h>
#include <linux/percpu.h>
#include <linux/sched/isolation.h>
+#include <linux/kpkeys.h>
#include <asm/arm_pmuv3.h>
#include <asm/cpu.h>
@@ -2451,8 +2452,10 @@ static void cpu_enable_mops(const struct arm64_cpu_capabilities *__unused)
#ifdef CONFIG_ARM64_POE
static void cpu_enable_poe(const struct arm64_cpu_capabilities *__unused)
{
- sysreg_clear_set(REG_TCR2_EL1, 0, TCR2_EL1_E0POE);
+ write_sysreg_s(POR_EL1_INIT, SYS_POR_EL1);
+ sysreg_clear_set(REG_TCR2_EL1, 0, TCR2_EL1_E0POE | TCR2_EL1_POE);
sysreg_clear_set(CPACR_EL1, 0, CPACR_EL1_E0POE);
+ isb();
}
#endif
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 10/24] memblock: Move INIT_MEMBLOCK_* macros to header
From: Kevin Brodsky @ 2026-05-26 11:15 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
The upcoming page table allocator for the kpkeys_hardened_pgtables
feature will need to know the maximum number of memblock regions.
Move the corresponding macros to <linux/memblock.h> to allow that.
INIT_MEMBLOCK_{RESERVED,MEMORY}_REGIONS may be overridden, but this
should be fine as only arm64 and loong currently do that and the
relevant header is already (indirectly) included by
<linux/memblock.h>.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/memblock.h | 11 +++++++++++
mm/memblock.c | 11 -----------
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/include/linux/memblock.h b/include/linux/memblock.h
index b0f750d22a7b..e58b2c859b95 100644
--- a/include/linux/memblock.h
+++ b/include/linux/memblock.h
@@ -24,6 +24,17 @@ extern unsigned long max_pfn;
*/
extern unsigned long long max_possible_pfn;
+#define INIT_MEMBLOCK_REGIONS 128
+#define INIT_PHYSMEM_REGIONS 4
+
+#ifndef INIT_MEMBLOCK_RESERVED_REGIONS
+#define INIT_MEMBLOCK_RESERVED_REGIONS INIT_MEMBLOCK_REGIONS
+#endif
+
+#ifndef INIT_MEMBLOCK_MEMORY_REGIONS
+#define INIT_MEMBLOCK_MEMORY_REGIONS INIT_MEMBLOCK_REGIONS
+#endif
+
/**
* enum memblock_flags - definition of memory region attributes
* @MEMBLOCK_NONE: no special request
diff --git a/mm/memblock.c b/mm/memblock.c
index a6a1c91e276d..e64ad9c72dc3 100644
--- a/mm/memblock.c
+++ b/mm/memblock.c
@@ -30,17 +30,6 @@
#include "internal.h"
-#define INIT_MEMBLOCK_REGIONS 128
-#define INIT_PHYSMEM_REGIONS 4
-
-#ifndef INIT_MEMBLOCK_RESERVED_REGIONS
-# define INIT_MEMBLOCK_RESERVED_REGIONS INIT_MEMBLOCK_REGIONS
-#endif
-
-#ifndef INIT_MEMBLOCK_MEMORY_REGIONS
-#define INIT_MEMBLOCK_MEMORY_REGIONS INIT_MEMBLOCK_REGIONS
-#endif
-
/**
* DOC: memblock overview
*
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 11/24] mm: kpkeys: Introduce kpkeys_hardened_pgtables feature
From: Kevin Brodsky @ 2026-05-26 11:16 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
kpkeys_hardened_pgtables is a hardening feature based on kpkeys. It
aims to prevent the corruption of page tables by: 1. mapping all
page table pages, both kernel and user, with a privileged pkey
(KPKEYS_PKEY_PGTABLES), and 2. granting write access to that pkey
only when running at in a privileged kpkeys context
(KPKEYS_CTX_PGTABLES). This patch introduces basic infrastructure;
the implementation of both aspects will follow.
The feature is exposed as CONFIG_KPKEYS_HARDENED_PGTABLES; it
requires explicit architecture opt-in by selecting
ARCH_HAS_KPKEYS_HARDENED_PGTABLES, since much of the page table
handling is arch-specific.
Because this feature relies on kpkeys being available and enabled,
and modifies attributes of the linear map, it must be inactive on
boot. kpkeys_hardened_pgtables_init() enables it by toggling a
static key; this function must be called by supported architectures
in mem_init(), before any call to pagetable_alloc() is made.
Supported architectures must also provide
arch_supports_kpkeys_early() in <asm/kpkeys.h>. This will be used
during early boot to detect whether kpkeys_hardened_pgtables is
going to be enabled (e.g. to decide how to allocate early page
tables).
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/asm-generic/kpkeys.h | 4 ++++
include/linux/kpkeys.h | 40 +++++++++++++++++++++++++++++++++++++++-
mm/Kconfig | 3 +++
mm/Makefile | 1 +
mm/kpkeys_hardened_pgtables.c | 16 ++++++++++++++++
security/Kconfig.hardening | 12 ++++++++++++
6 files changed, 75 insertions(+), 1 deletion(-)
diff --git a/include/asm-generic/kpkeys.h b/include/asm-generic/kpkeys.h
index ab819f157d6a..cec92334a9f3 100644
--- a/include/asm-generic/kpkeys.h
+++ b/include/asm-generic/kpkeys.h
@@ -2,6 +2,10 @@
#ifndef __ASM_GENERIC_KPKEYS_H
#define __ASM_GENERIC_KPKEYS_H
+#ifndef KPKEYS_PKEY_PGTABLES
+#define KPKEYS_PKEY_PGTABLES 1
+#endif
+
#ifndef KPKEYS_PKEY_DEFAULT
#define KPKEYS_PKEY_DEFAULT 0
#endif
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
index 0ce0db80b4ee..d413a17f8882 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -4,11 +4,13 @@
#include <linux/bug.h>
#include <linux/cleanup.h>
+#include <linux/jump_label.h>
#define KPKEYS_CTX_DEFAULT 0
+#define KPKEYS_CTX_PGTABLES 1
#define KPKEYS_CTX_MIN KPKEYS_CTX_DEFAULT
-#define KPKEYS_CTX_MAX KPKEYS_CTX_DEFAULT
+#define KPKEYS_CTX_MAX KPKEYS_CTX_PGTABLES
/*
* ... is used to discard extra arguments - this allows users of this macro
@@ -119,4 +121,40 @@ static inline bool kpkeys_enabled(void)
#endif /* CONFIG_ARCH_HAS_KPKEYS */
+#ifdef CONFIG_KPKEYS_HARDENED_PGTABLES
+
+DECLARE_STATIC_KEY_FALSE(kpkeys_hardened_pgtables_key);
+
+static inline bool kpkeys_hardened_pgtables_enabled(void)
+{
+ return static_branch_unlikely(&kpkeys_hardened_pgtables_key);
+}
+
+static inline bool kpkeys_hardened_pgtables_early_enabled(void)
+{
+ return arch_supports_kpkeys_early();
+}
+
+/*
+ * Should be called from mem_init(): as soon as the buddy allocator becomes
+ * available and before any call to pagetable_alloc().
+ */
+void kpkeys_hardened_pgtables_init(void);
+
+#else /* CONFIG_KPKEYS_HARDENED_PGTABLES */
+
+static inline bool kpkeys_hardened_pgtables_enabled(void)
+{
+ return false;
+}
+
+static inline bool kpkeys_hardened_pgtables_early_enabled(void)
+{
+ return false;
+}
+
+static inline void kpkeys_hardened_pgtables_init(void) {}
+
+#endif /* CONFIG_KPKEYS_HARDENED_PGTABLES */
+
#endif /* _LINUX_KPKEYS_H */
diff --git a/mm/Kconfig b/mm/Kconfig
index 819fb0d7b7bd..042541d28692 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -1244,6 +1244,9 @@ config ARCH_HAS_PKEYS
bool
config ARCH_HAS_KPKEYS
bool
+config ARCH_HAS_KPKEYS_HARDENED_PGTABLES
+ bool
+ depends on ARCH_HAS_KPKEYS
config ARCH_USES_PG_ARCH_2
bool
diff --git a/mm/Makefile b/mm/Makefile
index 8ad2ab08244e..7603e6051afa 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -150,3 +150,4 @@ obj-$(CONFIG_SHRINKER_DEBUG) += shrinker_debug.o
obj-$(CONFIG_EXECMEM) += execmem.o
obj-$(CONFIG_TMPFS_QUOTA) += shmem_quota.o
obj-$(CONFIG_LAZY_MMU_MODE_KUNIT_TEST) += tests/lazy_mmu_mode_kunit.o
+obj-$(CONFIG_KPKEYS_HARDENED_PGTABLES) += kpkeys_hardened_pgtables.o
diff --git a/mm/kpkeys_hardened_pgtables.c b/mm/kpkeys_hardened_pgtables.c
new file mode 100644
index 000000000000..763f267bbfe4
--- /dev/null
+++ b/mm/kpkeys_hardened_pgtables.c
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/kpkeys.h>
+#include <linux/mm.h>
+
+#include <kunit/visibility.h>
+
+__ro_after_init DEFINE_STATIC_KEY_FALSE(kpkeys_hardened_pgtables_key);
+EXPORT_SYMBOL_IF_KUNIT(kpkeys_hardened_pgtables_key);
+
+void __init kpkeys_hardened_pgtables_init(void)
+{
+ if (!kpkeys_enabled())
+ return;
+
+ static_branch_enable(&kpkeys_hardened_pgtables_key);
+}
diff --git a/security/Kconfig.hardening b/security/Kconfig.hardening
index 86f8768c63d4..fdaf977d4626 100644
--- a/security/Kconfig.hardening
+++ b/security/Kconfig.hardening
@@ -275,6 +275,18 @@ config BUG_ON_DATA_CORRUPTION
If unsure, say N.
+config KPKEYS_HARDENED_PGTABLES
+ bool "Harden page tables using kernel pkeys"
+ depends on ARCH_HAS_KPKEYS_HARDENED_PGTABLES
+ help
+ This option makes all page tables mostly read-only by
+ allocating them with a non-default protection key (pkey) and
+ only enabling write access to that pkey in routines that are
+ expected to write to page table entries.
+
+ This option has no effect if the system does not support
+ kernel pkeys.
+
endmenu
config CC_HAS_RANDSTRUCT
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 12/24] mm: kpkeys: Protect regular page tables
From: Kevin Brodsky @ 2026-05-26 11:16 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
If the kpkeys_hardened_pgtables feature is enabled, page table pages
(PTPs) should be protected by modifying the linear mapping to map
them with a privileged pkey (KPKEYS_PKEY_PGTABLES). This patch
introduces a new page allocator for that purpose:
* kpkeys_pgtable_alloc() allocates a new PTP and sets the linear
mapping to KPKEYS_PKEY_PGTABLES for that page
* kpkeys_pgtable_free() frees such a PTP and restores the linear
mapping to the default pkey
This interface is then hooked into pagetable_alloc() and
pagetable_free(), protecting all page tables created once the buddy
allocator is available. Early page tables are allocated in other
ways and will be protected in subsequent patches.
This implementation of kpkeys_pgtable_{alloc,free}() is minimal and
relies on the linear map being fully PTE-mapped - otherwise
calling set_memory_pkey() on a single page may result in splitting a
block mapping, which in turn requires allocating a new PTP. A more
elaborate implementation could be added later to handle this
situation.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/kpkeys.h | 10 +++++++++
include/linux/mm.h | 14 +++++++++++--
mm/kpkeys_hardened_pgtables.c | 47 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 69 insertions(+), 2 deletions(-)
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
index d413a17f8882..c7529f9e9f97 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -135,6 +135,9 @@ static inline bool kpkeys_hardened_pgtables_early_enabled(void)
return arch_supports_kpkeys_early();
}
+struct page *kpkeys_pgtable_alloc(gfp_t gfp, unsigned int order);
+void kpkeys_pgtable_free(struct page *page, unsigned int order);
+
/*
* Should be called from mem_init(): as soon as the buddy allocator becomes
* available and before any call to pagetable_alloc().
@@ -153,6 +156,13 @@ static inline bool kpkeys_hardened_pgtables_early_enabled(void)
return false;
}
+static inline struct page *kpkeys_pgtable_alloc(gfp_t gfp, unsigned int order)
+{
+ return NULL;
+}
+
+static inline void kpkeys_pgtable_free(struct page *page, unsigned int order) {}
+
static inline void kpkeys_hardened_pgtables_init(void) {}
#endif /* CONFIG_KPKEYS_HARDENED_PGTABLES */
diff --git a/include/linux/mm.h b/include/linux/mm.h
index af23453e9dbd..7b95b2351763 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -37,6 +37,7 @@
#include <linux/bitmap.h>
#include <linux/bitops.h>
#include <linux/iommu-debug-pagealloc.h>
+#include <linux/kpkeys.h>
struct mempolicy;
struct anon_vma;
@@ -3648,7 +3649,12 @@ static inline bool ptdesc_test_kernel(const struct ptdesc *ptdesc)
*/
static inline struct ptdesc *pagetable_alloc_noprof(gfp_t gfp, unsigned int order)
{
- struct page *page = alloc_pages_noprof(gfp | __GFP_COMP, order);
+ struct page *page;
+
+ if (kpkeys_hardened_pgtables_enabled())
+ page = kpkeys_pgtable_alloc(gfp | __GFP_COMP, order);
+ else
+ page = alloc_pages_noprof(gfp | __GFP_COMP, order);
return page_ptdesc(page);
}
@@ -3657,8 +3663,12 @@ static inline struct ptdesc *pagetable_alloc_noprof(gfp_t gfp, unsigned int orde
static inline void __pagetable_free(struct ptdesc *pt)
{
struct page *page = ptdesc_page(pt);
+ unsigned int order = compound_order(page);
- __free_pages(page, compound_order(page));
+ if (kpkeys_hardened_pgtables_enabled())
+ kpkeys_pgtable_free(page, order);
+ else
+ __free_pages(page, order);
}
#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE
diff --git a/mm/kpkeys_hardened_pgtables.c b/mm/kpkeys_hardened_pgtables.c
index 763f267bbfe4..fff7e2a64b64 100644
--- a/mm/kpkeys_hardened_pgtables.c
+++ b/mm/kpkeys_hardened_pgtables.c
@@ -1,12 +1,59 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/kpkeys.h>
#include <linux/mm.h>
+#include <linux/set_memory.h>
#include <kunit/visibility.h>
__ro_after_init DEFINE_STATIC_KEY_FALSE(kpkeys_hardened_pgtables_key);
EXPORT_SYMBOL_IF_KUNIT(kpkeys_hardened_pgtables_key);
+static int set_pkey_pgtable(struct page *page, unsigned int nr_pages)
+{
+ unsigned long addr = (unsigned long)page_address(page);
+ int ret;
+
+ ret = set_memory_pkey(addr, nr_pages, KPKEYS_PKEY_PGTABLES);
+
+ WARN_ON(ret);
+ return ret;
+}
+
+static int set_pkey_default(struct page *page, unsigned int nr_pages)
+{
+ unsigned long addr = (unsigned long)page_address(page);
+ int ret;
+
+ ret = set_memory_pkey(addr, nr_pages, KPKEYS_PKEY_DEFAULT);
+
+ WARN_ON(ret);
+ return ret;
+}
+
+struct page *kpkeys_pgtable_alloc(gfp_t gfp, unsigned int order)
+{
+ struct page *page;
+ int ret;
+
+ page = alloc_pages_noprof(gfp, order);
+ if (!page)
+ return page;
+
+ ret = set_pkey_pgtable(page, 1 << order);
+ if (ret) {
+ __free_pages(page, order);
+ return NULL;
+ }
+
+ return page;
+}
+
+void kpkeys_pgtable_free(struct page *page, unsigned int order)
+{
+ set_pkey_default(page, 1 << order);
+ __free_pages(page, order);
+}
+
void __init kpkeys_hardened_pgtables_init(void)
{
if (!kpkeys_enabled())
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 13/24] mm: kpkeys: Introduce early page table allocator
From: Kevin Brodsky @ 2026-05-26 11:16 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
The kpkeys_hardened_pgtables feature aims to protect all page table
pages (PTPs) by mapping them with a privileged pkey. This is primarily
handled by kpkeys_pgtable_alloc(), called from pagetable_alloc().
However, this does not cover PTPs allocated early, before the
buddy allocator is available. These PTPs are allocated by architecture
code, either 1. from static pools or 2. using the memblock allocator,
and should also be protected.
This patch addresses the second category: PTPs allocated via memblock.
Such PTPs are notably used to create the linear map. Protecting them as
soon as they are allocated would require modifying the linear map while
it is being created, which seems at best difficult. Instead, a
simple allocator is introduced, obtaining pages from memblock and
keeping track of all allocated ranges to set their pkey once it is
safe to do so. PTPs allocated at that stage are not freed, so there
is no need to manage a free list.
Since kpkeys_hardened_pgtables currently requires the linear map to
be PTE-mapped, we can directly allocate page by page using memblock,
without intermediate cache. We rely on memblock allocating
contiguous pages to minimise the number of tracked ranges.
The number of PTPs required to create the linear map is proportional to
the amount of available memory, which means it may be large. At such
an early point, the memblock allocator may however only track a
limited number of regions, and we size the tracking array
(allocated_ranges) accordingly. The array may be quite large as a
result (16KB on arm64), but it is discarded once boot has completed.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/kpkeys.h | 7 +++
mm/kpkeys_hardened_pgtables.c | 115 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 122 insertions(+)
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
index c7529f9e9f97..0e246354e95c 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -144,6 +144,8 @@ void kpkeys_pgtable_free(struct page *page, unsigned int order);
*/
void kpkeys_hardened_pgtables_init(void);
+phys_addr_t kpkeys_physmem_pgtable_alloc(void);
+
#else /* CONFIG_KPKEYS_HARDENED_PGTABLES */
static inline bool kpkeys_hardened_pgtables_enabled(void)
@@ -165,6 +167,11 @@ static inline void kpkeys_pgtable_free(struct page *page, unsigned int order) {}
static inline void kpkeys_hardened_pgtables_init(void) {}
+static inline phys_addr_t kpkeys_physmem_pgtable_alloc(void)
+{
+ return 0;
+}
+
#endif /* CONFIG_KPKEYS_HARDENED_PGTABLES */
#endif /* _LINUX_KPKEYS_H */
diff --git a/mm/kpkeys_hardened_pgtables.c b/mm/kpkeys_hardened_pgtables.c
index fff7e2a64b64..13af4930db3d 100644
--- a/mm/kpkeys_hardened_pgtables.c
+++ b/mm/kpkeys_hardened_pgtables.c
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/kpkeys.h>
+#include <linux/memblock.h>
#include <linux/mm.h>
#include <linux/set_memory.h>
@@ -30,6 +31,9 @@ static int set_pkey_default(struct page *page, unsigned int nr_pages)
return ret;
}
+/* pkeys physmem allocator (PPA) - implemented below */
+static void ppa_finalize(void);
+
struct page *kpkeys_pgtable_alloc(gfp_t gfp, unsigned int order)
{
struct page *page;
@@ -60,4 +64,115 @@ void __init kpkeys_hardened_pgtables_init(void)
return;
static_branch_enable(&kpkeys_hardened_pgtables_key);
+
+ ppa_finalize();
+}
+
+/*
+ * pkeys physmem allocator (PPA): allocator for very early page tables
+ * (especially for creating the linear map), based on memblock. Allocated
+ * ranges are tracked so that their pkey can be set once it is safe to do so.
+ */
+
+/*
+ * We may have to track many ranges when allocating page tables for the linear
+ * map, as their number grows with the amount of available memory. Assuming that
+ * memblock returns contiguous blocks whenever possible, the number of ranges
+ * to track cannot however exceed the number of regions that memblock itself
+ * tracks. memblock_allow_resize() hasn't been called yet at that point, so
+ * that limit is the size of the statically allocated array.
+ */
+#define PHYSMEM_MAX_RANGES INIT_MEMBLOCK_MEMORY_REGIONS
+
+struct physmem_range {
+ phys_addr_t addr;
+ phys_addr_t size;
+};
+
+struct pkeys_physmem_allocator {
+ struct physmem_range allocated_ranges[PHYSMEM_MAX_RANGES];
+ unsigned int nr_allocated_ranges;
+};
+
+static struct pkeys_physmem_allocator pkeys_physmem_allocator __initdata;
+
+static int __init set_pkey_pgtable_phys(phys_addr_t pa, phys_addr_t size)
+{
+ unsigned long addr = (unsigned long)__va(pa);
+ int ret;
+
+ ret = set_memory_pkey(addr, size / PAGE_SIZE, KPKEYS_PKEY_PGTABLES);
+
+ WARN_ON(ret);
+ return ret;
+}
+
+static bool __init ppa_try_extend_last_range(phys_addr_t addr, phys_addr_t size)
+{
+ struct pkeys_physmem_allocator *ppa = &pkeys_physmem_allocator;
+ struct physmem_range *range;
+
+ if (!ppa->nr_allocated_ranges)
+ return false;
+
+ range = &ppa->allocated_ranges[ppa->nr_allocated_ranges - 1];
+
+ /* Merge the new range into the last range if they are contiguous */
+ if (addr == range->addr + range->size) {
+ range->size += size;
+ return true;
+ } else if (addr + size == range->addr) {
+ range->addr -= size;
+ range->size += size;
+ return true;
+ }
+
+ return false;
+}
+
+static void __init ppa_register_allocated_range(phys_addr_t addr,
+ phys_addr_t size)
+{
+ struct pkeys_physmem_allocator *ppa = &pkeys_physmem_allocator;
+ struct physmem_range *range;
+
+ if (!addr)
+ return;
+
+ if (ppa_try_extend_last_range(addr, size))
+ return;
+
+ /* Could not extend the last range, create a new one */
+ if (WARN_ON(ppa->nr_allocated_ranges >= PHYSMEM_MAX_RANGES))
+ return;
+
+ range = &ppa->allocated_ranges[ppa->nr_allocated_ranges++];
+ range->addr = addr;
+ range->size = size;
+}
+
+static void __init ppa_finalize(void)
+{
+ struct pkeys_physmem_allocator *ppa = &pkeys_physmem_allocator;
+
+ for (unsigned int i = 0; i < ppa->nr_allocated_ranges; i++) {
+ struct physmem_range *range = &ppa->allocated_ranges[i];
+
+ set_pkey_pgtable_phys(range->addr, range->size);
+ }
+}
+
+phys_addr_t __ref kpkeys_physmem_pgtable_alloc(void)
+{
+ size_t size = PAGE_SIZE;
+ phys_addr_t addr;
+
+ addr = memblock_phys_alloc_range(size, size, 0,
+ MEMBLOCK_ALLOC_NOLEAKTRACE);
+ if (!addr)
+ return addr;
+
+ ppa_register_allocated_range(addr, size);
+
+ return addr;
}
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 14/24] mm: kpkeys: Protect vmemmap page tables
From: Kevin Brodsky @ 2026-05-26 11:16 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
When the kpkeys_hardened_pgtables feature is enabled, make sure that
vmemmap page tables are protected by using:
* The standard pagetable_alloc() if the buddy allocator is
available, as it already allocates protected memory.
* The memblock-based kpkeys allocator for early allocations.
These allocators are not NUMA-aware, so the page tables may be
allocated on any node. This could potentially incur some overhead on
large NUMA systems.
The arm64 hotplug code is also amended to use a matching
pagetable_free(), ensuring that the pkey is reset when the page
tables are freed. x86 already uses pagetable_free() on that path.
Unlike in vmemmap_alloc_block(), __GFP_RETRY_MAYFAIL is not used as
it isn't justified for allocating page tables - this disables the
OOM and we do not have a fallback if we fail to allocate page
tables. See previous discussion linked below.
Link: https://lore.kernel.org/all/38d2a358-4146-bfc9-2a4f-68ce02f75c94@suse.cz/
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
This is a minimal patch to protect vmemmmap page tables. More work
may be needed here:
* Restoring NUMA awareness
* Moving the arm64 change to a separate commit?
* General refactoring of how these page tables are allocated: since
we are not using the standard per-level functions (e.g.
pmd_alloc()), we are not calling pagetable_*_ctor() or
ptdesc_set_kernel(). [Maybe that doesn't matter because these page
tables can only be freed via vmemmap_free()?]
---
arch/arm64/mm/mmu.c | 2 +-
mm/sparse-vmemmap.c | 33 +++++++++++++++++++++++++--------
2 files changed, 26 insertions(+), 9 deletions(-)
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index 493310cf0486..dc69553d6326 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -1441,7 +1441,7 @@ static void free_hotplug_page_range(struct page *page, size_t size,
static void free_hotplug_pgtable_page(struct page *page)
{
- free_hotplug_page_range(page, PAGE_SIZE, NULL);
+ pagetable_free(page_ptdesc(page));
}
static bool pgtable_range_aligned(unsigned long start, unsigned long end,
diff --git a/mm/sparse-vmemmap.c b/mm/sparse-vmemmap.c
index 6eadb9d116e4..c93f5b9f4a26 100644
--- a/mm/sparse-vmemmap.c
+++ b/mm/sparse-vmemmap.c
@@ -184,13 +184,29 @@ pte_t * __meminit vmemmap_pte_populate(pmd_t *pmd, unsigned long addr, int node,
return pte;
}
-static void * __meminit vmemmap_alloc_block_zero(unsigned long size, int node)
+static void * __meminit vmemmap_alloc_pgtable(int node)
{
- void *p = vmemmap_alloc_block(size, node);
+ void *p;
+
+ if (slab_is_available()) {
+ gfp_t gfp = GFP_KERNEL | __GFP_ZERO;
+ struct ptdesc *ptdesc = pagetable_alloc(gfp, 0);
+
+ return ptdesc ? ptdesc_address(ptdesc) : NULL;
+ }
+
+ if (kpkeys_hardened_pgtables_early_enabled()) {
+ phys_addr_t phys = kpkeys_physmem_pgtable_alloc();
+
+ p = phys ? phys_to_virt(phys) : NULL;
+ } else {
+ p = __earlyonly_bootmem_alloc(node, PAGE_SIZE, PAGE_SIZE,
+ __pa(MAX_DMA_ADDRESS));
+ }
if (!p)
return NULL;
- memset(p, 0, size);
+ memset(p, 0, PAGE_SIZE);
return p;
}
@@ -199,7 +215,7 @@ pmd_t * __meminit vmemmap_pmd_populate(pud_t *pud, unsigned long addr, int node)
{
pmd_t *pmd = pmd_offset(pud, addr);
if (pmd_none(*pmd)) {
- void *p = vmemmap_alloc_block_zero(PAGE_SIZE, node);
+ void *p = vmemmap_alloc_pgtable(node);
if (!p)
return NULL;
kernel_pte_init(p);
@@ -212,7 +228,7 @@ pud_t * __meminit vmemmap_pud_populate(p4d_t *p4d, unsigned long addr, int node)
{
pud_t *pud = pud_offset(p4d, addr);
if (pud_none(*pud)) {
- void *p = vmemmap_alloc_block_zero(PAGE_SIZE, node);
+ void *p = vmemmap_alloc_pgtable(node);
if (!p)
return NULL;
pmd_init(p);
@@ -225,7 +241,7 @@ p4d_t * __meminit vmemmap_p4d_populate(pgd_t *pgd, unsigned long addr, int node)
{
p4d_t *p4d = p4d_offset(pgd, addr);
if (p4d_none(*p4d)) {
- void *p = vmemmap_alloc_block_zero(PAGE_SIZE, node);
+ void *p = vmemmap_alloc_pgtable(node);
if (!p)
return NULL;
pud_init(p);
@@ -238,7 +254,7 @@ pgd_t * __meminit vmemmap_pgd_populate(unsigned long addr, int node)
{
pgd_t *pgd = pgd_offset_k(addr);
if (pgd_none(*pgd)) {
- void *p = vmemmap_alloc_block_zero(PAGE_SIZE, node);
+ void *p = vmemmap_alloc_pgtable(node);
if (!p)
return NULL;
pgd_populate_kernel(addr, pgd, p);
@@ -351,10 +367,11 @@ static __meminit struct page *vmemmap_get_tail(unsigned int order, struct zone *
* memmap_init().
*/
- p = vmemmap_alloc_block_zero(PAGE_SIZE, node);
+ p = vmemmap_alloc_block(PAGE_SIZE, node);
if (!p)
return NULL;
+ memset(p, 0, PAGE_SIZE);
tail = virt_to_page(p);
zone->vmemmap_tails[idx] = tail;
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 15/24] mm: kpkeys: Introduce hook for protecting static page tables
From: Kevin Brodsky @ 2026-05-26 11:16 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
The kpkeys_hardened_pgtables infrastructure introduced so far allows
compatible architectures to protect all page table pages (PTPs)
allocated at runtime (first via memblock, then the buddy allocator).
Some PTPs are however required even earlier, before any allocator is
available. This is typically needed for mapping the kernel image
itself.
These PTPs are at least as sensitive as those allocated later on,
and should be protected by mapping them with the privileged pkey.
Exactly how these pages are obtained is entirely arch-specific, so
we introduce a hook to let architectures that implement
kpkeys_hardened_pgtables do the right thing.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/kpkeys.h | 4 ++++
mm/kpkeys_hardened_pgtables.c | 1 +
2 files changed, 5 insertions(+)
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
index 0e246354e95c..c0ae7e1fc382 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -146,6 +146,10 @@ void kpkeys_hardened_pgtables_init(void);
phys_addr_t kpkeys_physmem_pgtable_alloc(void);
+#ifndef arch_kpkeys_protect_static_pgtables
+static inline void arch_kpkeys_protect_static_pgtables(void) {}
+#endif
+
#else /* CONFIG_KPKEYS_HARDENED_PGTABLES */
static inline bool kpkeys_hardened_pgtables_enabled(void)
diff --git a/mm/kpkeys_hardened_pgtables.c b/mm/kpkeys_hardened_pgtables.c
index 13af4930db3d..269de610d744 100644
--- a/mm/kpkeys_hardened_pgtables.c
+++ b/mm/kpkeys_hardened_pgtables.c
@@ -66,6 +66,7 @@ void __init kpkeys_hardened_pgtables_init(void)
static_branch_enable(&kpkeys_hardened_pgtables_key);
ppa_finalize();
+ arch_kpkeys_protect_static_pgtables();
}
/*
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 16/24] arm64: kpkeys: Implement arch_supports_kpkeys_early()
From: Kevin Brodsky @ 2026-05-26 11:16 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
We need to check if the kpkeys_hardened_pgtables feature is going to
be enabled very early during boot, to decide how to set up the
linear map and how to allocate early page tables. This happens even
before boot CPU features are detected.
Implement the arch_supports_kpkeys_early() helper by directly
checking if the boot CPU supports POE, if it is called before boot
CPU features are detected. It may also be called later, in which
case we simply check the POE feature.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/cpufeature.h | 12 ++++++++++++
arch/arm64/include/asm/kpkeys.h | 7 +++++++
2 files changed, 19 insertions(+)
diff --git a/arch/arm64/include/asm/cpufeature.h b/arch/arm64/include/asm/cpufeature.h
index 4de51f8d92cb..8722e9e62702 100644
--- a/arch/arm64/include/asm/cpufeature.h
+++ b/arch/arm64/include/asm/cpufeature.h
@@ -1078,6 +1078,18 @@ static inline bool cpu_has_lpa2(void)
#endif
}
+static inline bool cpu_has_poe(void)
+{
+ u64 mmfr3;
+
+ if (!IS_ENABLED(CONFIG_ARM64_POE))
+ return false;
+
+ mmfr3 = read_sysreg_s(SYS_ID_AA64MMFR3_EL1);
+ return cpuid_feature_extract_unsigned_field(mmfr3,
+ ID_AA64MMFR3_EL1_S1POE_SHIFT);
+}
+
#endif /* __ASSEMBLER__ */
#endif
diff --git a/arch/arm64/include/asm/kpkeys.h b/arch/arm64/include/asm/kpkeys.h
index 4dbfeb3dfcfe..fdf3ebe92810 100644
--- a/arch/arm64/include/asm/kpkeys.h
+++ b/arch/arm64/include/asm/kpkeys.h
@@ -21,6 +21,13 @@ static inline bool arch_supports_kpkeys(void)
return system_supports_poe();
}
+static inline bool arch_supports_kpkeys_early(void)
+{
+ /* POE is a boot feature */
+ return boot_capabilities_finalized() ?
+ system_supports_poe() : cpu_has_poe();
+}
+
#ifdef CONFIG_ARM64_POE
static inline u64 por_set_kpkeys_context(u64 por, int ctx)
--
2.51.2
^ permalink raw reply related
* [PATCH RFC v8 17/24] arm64: kpkeys: Support KPKEYS_CTX_PGTABLES
From: Kevin Brodsky @ 2026-05-26 11:16 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Vlastimil Babka, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner
In-Reply-To: <20260526-kpkeys-v8-0-eaaacdacc67c@arm.com>
Enable RW access to KPKEYS_PKEY_PGTABLES (used to map page table
pages) if switching to KPKEYS_CTX_PGTABLES, otherwise only grant RO
access.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/kpkeys.h | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/include/asm/kpkeys.h b/arch/arm64/include/asm/kpkeys.h
index fdf3ebe92810..c1daab643195 100644
--- a/arch/arm64/include/asm/kpkeys.h
+++ b/arch/arm64/include/asm/kpkeys.h
@@ -12,7 +12,8 @@
* Equivalent to por_set_kpkeys_context(0, KPKEYS_CTX_DEFAULT), but can also be
* used in assembly.
*/
-#define POR_EL1_INIT POR_ELx_PERM_PREP(KPKEYS_PKEY_DEFAULT, POE_RWX)
+#define POR_EL1_INIT (POR_ELx_PERM_PREP(KPKEYS_PKEY_DEFAULT, POE_RWX) | \
+ POR_ELx_PERM_PREP(KPKEYS_PKEY_PGTABLES, POE_R))
#ifndef __ASSEMBLY__
@@ -33,6 +34,8 @@ static inline bool arch_supports_kpkeys_early(void)
static inline u64 por_set_kpkeys_context(u64 por, int ctx)
{
por = por_elx_set_pkey_perms(por, KPKEYS_PKEY_DEFAULT, POE_RWX);
+ por = por_elx_set_pkey_perms(por, KPKEYS_PKEY_PGTABLES,
+ ctx == KPKEYS_CTX_PGTABLES ? POE_RW : POE_R);
return por;
}
--
2.51.2
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox