git: bdb561843e86 - main - linux: implement pkey_alloc, pkey_free and pkey_mprotect
Date: Sun, 16 Aug 2026 01:01:00 UTC
The branch main has been updated by dteske:
URL: https://cgit.FreeBSD.org/src/commit/?id=bdb561843e865eaa5bbdc5394ed9d9c91136240c
commit bdb561843e865eaa5bbdc5394ed9d9c91136240c
Author: Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-08-16 00:58:19 +0000
Commit: Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-08-16 00:58:43 +0000
linux: implement pkey_alloc, pkey_free and pkey_mprotect
Bridge the Linux memory protection key syscalls to FreeBSD's native
MPK support instead of returning ENOSYS. Modern Linux software
probes these at startup: Chromium-based browsers (found via
www/linux-brave) use protection keys for V8's heap and JIT
sandboxing, and glibc >= 2.27 exposes the full API.
pkey_alloc() allocates from a per-process bitmap kept in the process
emuldata (key 0 implicitly allocated, matching Linux's
mm_pkey_allocation_map; ENOSPC once keys 1..15 are exhausted or when
PKU is absent, as Linux returns on such hardware) and applies the
requested initial access rights to the calling thread's PKRU, located
in the XSAVE area via xsave_area_offset(). pkey_free() is
bookkeeping only: as on Linux, freeing neither untags pages nor
updates PKRU. pkey_mprotect() performs the protection change and
tags the range through amd64_pkru_update(), factored out of
sysarch(2)'s AMD64_SET_PKRU/AMD64_CLEAR_PKRU implementation so that
both share the same argument checking and map read lock
synchronization with a parallel pmap_vmspace_copy() on fork; tags die
with the mapping, matching Linux VMA semantics. A pkey of -1
degrades to plain mprotect.
The allocation map is inherited on fork and reset on exec. At exec
the Linux sysvecs initialize PKRU to 0x55555554, Linux's init_pkru
default (access disabled for keys 1..15), so memory tagged with a
not yet allocated key is inaccessible to threads that were never
granted rights -- the property V8's thread isolation relies on.
Setting PKRU at exec initializes the user FPU state slightly earlier
than the lazy first-use path; the state would be initialized moments
later in rtld/libc startup regardless. Protection key faults
already deliver SEGV_PKUERR through the existing siginfo
translation.
The common code carries no architecture ifdefs. Machine-dependent
state lives in struct linux_pemuldata_md, embedded in the process
emuldata in the manner of struct mdthread, and common code calls
per-arch lifecycle hooks (linux_pemuldata_init_md/_exec_md) and pkey
back ends after performing the parameter validation Linux applies
regardless of hardware support. On amd64 the implementation lives
in sys/amd64/linux/linux_pkru.c, compiled into linux_common and
serving both the 64-bit and 32-bit Linux ABIs. Elsewhere (arm64,
i386) linux_emul_md.c provides stubs returning what Linux returns on
hardware without protection keys (ENOSPC from pkey_alloc;
pkey_mprotect with a pkey of -1 acts as plain mprotect), so
applications take their normal no-PKU fallback instead of the ENOSYS
path.
PR: 297427
MFC after: 1 month
Reviewed by: kib
Differential Revision: https://reviews.freebsd.org/D58782
---
sys/amd64/amd64/sys_machdep.c | 85 +++++++-------
sys/amd64/linux/linux_emul_md.h | 35 ++++++
sys/amd64/linux/linux_pkru.c | 226 +++++++++++++++++++++++++++++++++++++
sys/amd64/linux/linux_sysvec.c | 4 +
sys/amd64/linux32/linux32_sysvec.c | 4 +
sys/arm64/linux/linux_emul_md.c | 52 +++++++++
sys/arm64/linux/linux_emul_md.h | 18 +++
sys/compat/linux/linux_dummy.c | 3 -
sys/compat/linux/linux_emul.c | 2 +
sys/compat/linux/linux_emul.h | 12 ++
sys/compat/linux/linux_misc.c | 22 ++++
sys/compat/linux/linux_mmap.c | 38 +++++++
sys/compat/linux/linux_mmap.h | 21 ++++
sys/i386/linux/linux_emul_md.c | 52 +++++++++
sys/i386/linux/linux_emul_md.h | 18 +++
sys/modules/linux/Makefile | 1 +
sys/modules/linux_common/Makefile | 10 +-
sys/x86/include/sysarch.h | 1 +
18 files changed, 554 insertions(+), 50 deletions(-)
diff --git a/sys/amd64/amd64/sys_machdep.c b/sys/amd64/amd64/sys_machdep.c
index 1df73a25c05e..2c6387d2a0d2 100644
--- a/sys/amd64/amd64/sys_machdep.c
+++ b/sys/amd64/amd64/sys_machdep.c
@@ -167,11 +167,44 @@ update_gdt_fsbase(struct thread *td, uint32_t base)
critical_exit();
}
+/*
+ * Tag (or untag, when 'clear' is true) a range of the calling
+ * process's address space with a protection key. The map read lock
+ * synchronizes with a parallel pmap_vmspace_copy() on fork.
+ *
+ * Shared between sysarch(2) and the Linuxulator's pkey_mprotect().
+ */
+int
+amd64_pkru_update(struct thread *td, uintptr_t addr, size_t len, u_int keyidx,
+ int flags, bool clear)
+{
+ struct vm_map *map;
+ vm_offset_t start, end;
+ int error;
+
+ MPASS(td == curthread);
+
+ map = &td->td_proc->p_vmspace->vm_map;
+ vm_map_lock_read(map);
+ if (len == 0 || !vm_map_check_boundary(map, addr, addr + len)) {
+ vm_map_unlock_read(map);
+ return (EINVAL);
+ }
+ start = trunc_page(addr);
+ end = round_page(addr + len);
+ if (clear)
+ error = pmap_pkru_clear(PCPU_GET(curpmap), start, end);
+ else
+ error = pmap_pkru_set(PCPU_GET(curpmap), start, end, keyidx,
+ flags);
+ vm_map_unlock_read(map);
+ return (error);
+}
+
int
sysarch(struct thread *td, struct sysarch_args *uap)
{
struct pcb *pcb;
- struct vm_map *map;
uint32_t i386base;
uint64_t a64base;
struct i386_ioperm_args iargs;
@@ -368,58 +401,20 @@ sysarch(struct thread *td, struct sysarch_args *uap)
break;
case I386_SET_PKRU:
- case AMD64_SET_PKRU: {
- vm_offset_t addr, start, end;
- vm_size_t len;
-
- addr = (uintptr_t)a64pkru.addr;
- len = a64pkru.len;
-
- /*
- * Read-lock the map to synchronize with parallel
- * pmap_vmspace_copy() on fork.
- */
- map = &td->td_proc->p_vmspace->vm_map;
- vm_map_lock_read(map);
- if (len == 0 || !vm_map_check_boundary(map, addr, addr + len)) {
- vm_map_unlock_read(map);
- error = EINVAL;
- break;
- }
- start = trunc_page(addr);
- end = round_page(addr + len);
- error = pmap_pkru_set(PCPU_GET(curpmap), start, end,
- a64pkru.keyidx, a64pkru.flags);
- vm_map_unlock_read(map);
+ case AMD64_SET_PKRU:
+ error = amd64_pkru_update(td, (uintptr_t)a64pkru.addr,
+ a64pkru.len, a64pkru.keyidx, a64pkru.flags, false);
break;
- }
case I386_CLEAR_PKRU:
- case AMD64_CLEAR_PKRU: {
- vm_offset_t addr, start, end;
- vm_size_t len;
-
+ case AMD64_CLEAR_PKRU:
if (a64pkru.flags != 0 || a64pkru.keyidx != 0) {
error = EINVAL;
break;
}
-
- addr = (uintptr_t)a64pkru.addr;
- len = a64pkru.len;
-
- map = &td->td_proc->p_vmspace->vm_map;
- vm_map_lock_read(map);
- if (len == 0 || !vm_map_check_boundary(map, addr, addr + len)) {
- vm_map_unlock_read(map);
- error = EINVAL;
- break;
- }
- start = trunc_page(addr);
- end = round_page(addr + len);
- error = pmap_pkru_clear(PCPU_GET(curpmap), start, end);
- vm_map_unlock_read(map);
+ error = amd64_pkru_update(td, (uintptr_t)a64pkru.addr,
+ a64pkru.len, 0, 0, true);
break;
- }
case AMD64_DISABLE_TLSBASE:
clear_pcb_flags(pcb, PCB_TLSBASE);
diff --git a/sys/amd64/linux/linux_emul_md.h b/sys/amd64/linux/linux_emul_md.h
new file mode 100644
index 000000000000..a5ea9c20e20a
--- /dev/null
+++ b/sys/amd64/linux/linux_emul_md.h
@@ -0,0 +1,35 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _AMD64_LINUX_EMUL_MD_H_
+#define _AMD64_LINUX_EMUL_MD_H_
+
+/*
+ * Machine-dependent part of the Linux process emuldata, embedded in
+ * struct linux_pemuldata as pem_md.
+ */
+struct linux_pemuldata_md {
+ uint32_t md_pkey_allocation_map; /* x86 protection keys */
+};
+
+/*
+ * Initial protection key allocation map: key 0 is the default key,
+ * implicitly allocated on Linux (mm_pkey_allocation_map is initialized
+ * to 0x1). Inherited on fork, reset on exec.
+ */
+#define LINUX_PKEY_INITIAL_MAP 0x1
+
+/*
+ * Initial PKRU at exec: access disabled for keys 1..15, key 0 open;
+ * the Linux init_pkru default.
+ */
+#define LINUX_PKRU_INIT 0x55555554
+
+struct thread;
+
+void linux_pkru_exec_init(struct thread *);
+
+#endif /* !_AMD64_LINUX_EMUL_MD_H_ */
diff --git a/sys/amd64/linux/linux_pkru.c b/sys/amd64/linux/linux_pkru.c
new file mode 100644
index 000000000000..159f8492ff35
--- /dev/null
+++ b/sys/amd64/linux/linux_pkru.c
@@ -0,0 +1,226 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+/*
+ * x86 memory protection keys (PKU) for the Linuxulator, serving both
+ * the 64-bit and 32-bit Linux ABIs.
+ *
+ * The PKRU register is directly user-visible: Linux programs read and
+ * write it with RDPKRU/WRPKRU, which execute natively. The kernel's
+ * part is key allocation bookkeeping (per address space: inherited on
+ * fork, reset on exec, as with Linux mm->context.pkey_allocation_map),
+ * tagging pages (pkey_mprotect), and applying the initial access
+ * rights of pkey_alloc() to the calling thread's PKRU.
+ */
+
+#include <sys/systm.h>
+#include <sys/imgact.h>
+#include <sys/lock.h>
+#include <sys/pcpu.h>
+#include <sys/proc.h>
+#include <sys/sx.h>
+
+#include <machine/cpufunc.h>
+#include <machine/fpu.h>
+#include <machine/md_var.h>
+#include <machine/pcb.h>
+#include <machine/specialreg.h>
+#include <machine/sysarch.h>
+#include <x86/x86_var.h>
+
+#include <compat/linux/linux_emul.h>
+#include <compat/linux/linux_mmap.h>
+
+static bool
+linux_pkey_supported(void)
+{
+
+ return ((cpu_stdext_feature2 & CPUID_STDEXT2_OSPKE) != 0);
+}
+
+/*
+ * Update the calling thread's PKRU: new value is (PKRU & keep) | set.
+ */
+static void
+linux_pkru_write(struct thread *td, uint32_t keep, uint32_t set)
+{
+ struct pcb *pcb;
+ struct xstate_hdr *hdr;
+ char *sa;
+ uint32_t *pkru;
+
+ MPASS(td == curthread);
+ pcb = td->td_pcb;
+
+ /*
+ * The critical section is held across the save area update to
+ * exclude preemption: a context switch could otherwise load the
+ * xsave area back into the CPU after fpugetregs(), and the
+ * stores below would then be lost to the next save.
+ */
+ critical_enter();
+ if ((pcb->pcb_flags & PCB_USERFPUINITDONE) != 0 &&
+ td == PCPU_GET(fpcurthread) && PCB_USER_FPU(pcb)) {
+ wrpkru((rdpkru() & keep) | set);
+ critical_exit();
+ return;
+ }
+
+ /*
+ * The user FPU state is in the PCB save area, or is not yet
+ * initialized, in which case fpugetregs() installs the initial
+ * state there.
+ */
+ (void)fpugetregs(td);
+ sa = (char *)get_pcb_user_save_td(td);
+ hdr = (struct xstate_hdr *)(sa + xsave_area_hdr_offset());
+ pkru = (uint32_t *)(sa + xsave_area_offset(xsave_mask,
+ XFEATURE_ENABLED_PKRU, false, false));
+ if ((hdr->xstate_bv & XFEATURE_ENABLED_PKRU) == 0) {
+ hdr->xstate_bv |= XFEATURE_ENABLED_PKRU;
+ *pkru = 0;
+ }
+ *pkru = (*pkru & keep) | set;
+ critical_exit();
+}
+
+/*
+ * Set the calling thread's PKRU access rights for the given key.
+ */
+static void
+linux_pkru_set_perm(struct thread *td, u_int keyidx, uint32_t rights)
+{
+
+ linux_pkru_write(td, ~(LINUX_PKEY_ACCESS_MASK << (keyidx * 2)),
+ rights << (keyidx * 2));
+}
+
+/*
+ * Called from the Linux sysvecs' exec_setregs. Linux initializes
+ * PKRU at exec to deny access to all keys but key 0
+ * (arch/x86/mm/pkeys.c init_pkru_value), so memory tagged with a not
+ * yet allocated key is inaccessible; FreeBSD's initial PKRU is 0.
+ * This initializes the user FPU state slightly earlier than the lazy
+ * first-use path; the state would be initialized moments later in
+ * rtld/libc startup regardless.
+ */
+void
+linux_pkru_exec_init(struct thread *td)
+{
+
+ if (!linux_pkey_supported())
+ return;
+ linux_pkru_write(td, 0, LINUX_PKRU_INIT);
+}
+
+/*
+ * Protection keys are a property of the address space: inherit the
+ * allocation map on fork, as Linux does. When a FreeBSD process is
+ * switching to the Linux ABI there is no parent emuldata; start from
+ * the initial map. The unlocked read is atomic on the aligned word;
+ * a pkey_alloc() racing the fork in another thread yields a valid
+ * serialization either way.
+ */
+void
+linux_pemuldata_init_md(struct thread *td, struct linux_pemuldata *pem)
+{
+ struct linux_pemuldata *ppem;
+
+ ppem = pem_find(td->td_proc);
+ if (ppem != NULL)
+ pem->pem_md.md_pkey_allocation_map =
+ ppem->pem_md.md_pkey_allocation_map;
+ else
+ pem->pem_md.md_pkey_allocation_map = LINUX_PKEY_INITIAL_MAP;
+}
+
+void
+linux_pemuldata_exec_md(struct linux_pemuldata *pem)
+{
+
+ pem->pem_md.md_pkey_allocation_map = LINUX_PKEY_INITIAL_MAP;
+}
+
+int
+linux_pkey_alloc_machdep(struct thread *td, uint64_t init_val)
+{
+ struct linux_pemuldata *pem;
+ uint32_t free_keys;
+ int key;
+
+ if (!linux_pkey_supported())
+ return (ENOSPC);
+
+ pem = pem_find(td->td_proc);
+ LINUX_PEM_XLOCK(pem);
+ free_keys = ~pem->pem_md.md_pkey_allocation_map &
+ ((1u << LINUX_PKEY_MAX) - 1) & ~LINUX_PKEY_INITIAL_MAP;
+ if (free_keys == 0) {
+ LINUX_PEM_XUNLOCK(pem);
+ return (ENOSPC);
+ }
+ key = ffs(free_keys) - 1;
+ pem->pem_md.md_pkey_allocation_map |= 1u << key;
+ LINUX_PEM_XUNLOCK(pem);
+
+ linux_pkru_set_perm(td, key, init_val);
+ td->td_retval[0] = key;
+ return (0);
+}
+
+int
+linux_pkey_free_machdep(struct thread *td, int pkey)
+{
+ struct linux_pemuldata *pem;
+
+ if (!linux_pkey_supported())
+ return (EINVAL);
+
+ pem = pem_find(td->td_proc);
+ LINUX_PEM_XLOCK(pem);
+ if ((pem->pem_md.md_pkey_allocation_map & (1u << pkey)) == 0) {
+ LINUX_PEM_XUNLOCK(pem);
+ return (EINVAL);
+ }
+ pem->pem_md.md_pkey_allocation_map &= ~(1u << pkey);
+ LINUX_PEM_XUNLOCK(pem);
+
+ /*
+ * As on Linux, freeing a key neither untags pages nor updates
+ * PKRU; that is the application's responsibility.
+ */
+ return (0);
+}
+
+int
+linux_pkey_mprotect_machdep(struct thread *td, uintptr_t addr, size_t len,
+ int prot, int pkey)
+{
+ struct linux_pemuldata *pem;
+ int error;
+
+ if (!linux_pkey_supported())
+ return (EINVAL);
+
+ pem = pem_find(td->td_proc);
+ LINUX_PEM_SLOCK(pem);
+ if ((pem->pem_md.md_pkey_allocation_map & (1u << pkey)) == 0) {
+ LINUX_PEM_SUNLOCK(pem);
+ return (EINVAL);
+ }
+ LINUX_PEM_SUNLOCK(pem);
+
+ error = linux_mprotect_common(td, addr, len, prot);
+ if (error != 0 || len == 0)
+ return (error);
+
+ /*
+ * Tag the range; a pkey of 0 untags it. The tag is not
+ * persistent: it dies with the mapping, matching Linux VMA
+ * semantics.
+ */
+ return (amd64_pkru_update(td, addr, len, pkey, 0, pkey == 0));
+}
diff --git a/sys/amd64/linux/linux_sysvec.c b/sys/amd64/linux/linux_sysvec.c
index 890cf01c46a0..ecb497c61a1a 100644
--- a/sys/amd64/linux/linux_sysvec.c
+++ b/sys/amd64/linux/linux_sysvec.c
@@ -57,6 +57,7 @@
#include <x86/linux/linux_x86.h>
#include <amd64/linux/linux.h>
+#include <amd64/linux/linux_emul_md.h>
#include <amd64/linux/linux_proto.h>
#include <compat/linux/linux_elf.h>
#include <compat/linux/linux_emul.h>
@@ -271,6 +272,9 @@ linux_exec_setregs(struct thread *td, struct image_params *imgp,
* clean FP state if it uses the FPU again.
*/
fpstate_drop(td);
+
+ /* Linux processes start with PKRU denying unallocated keys. */
+ linux_pkru_exec_init(td);
}
static int
diff --git a/sys/amd64/linux32/linux32_sysvec.c b/sys/amd64/linux32/linux32_sysvec.c
index 735ebb151017..c4de547b2bf5 100644
--- a/sys/amd64/linux32/linux32_sysvec.c
+++ b/sys/amd64/linux32/linux32_sysvec.c
@@ -61,6 +61,7 @@
#include <machine/trap.h>
#include <x86/linux/linux_x86.h>
+#include <amd64/linux/linux_emul_md.h>
#include <amd64/linux32/linux.h>
#include <amd64/linux32/linux32_proto.h>
#include <compat/linux/linux_elf.h>
@@ -609,6 +610,9 @@ linux_exec_setregs(struct thread *td, struct image_params *imgp,
x86_clear_dbregs(pcb);
fpstate_drop(td);
+
+ /* Linux processes start with PKRU denying unallocated keys. */
+ linux_pkru_exec_init(td);
}
/*
diff --git a/sys/arm64/linux/linux_emul_md.c b/sys/arm64/linux/linux_emul_md.c
new file mode 100644
index 000000000000..9dd507ad4f49
--- /dev/null
+++ b/sys/arm64/linux/linux_emul_md.c
@@ -0,0 +1,52 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/proc.h>
+
+#include <compat/linux/linux_emul.h>
+#include <compat/linux/linux_mmap.h>
+
+/* No machine-dependent emuldata state yet. */
+
+void
+linux_pemuldata_init_md(struct thread *td, struct linux_pemuldata *pem)
+{
+}
+
+void
+linux_pemuldata_exec_md(struct linux_pemuldata *pem)
+{
+}
+
+/*
+ * Protection key back ends: behave as Linux does on hardware without
+ * protection keys. pkey_alloc() reports no free keys and only the
+ * default key semantics remain.
+ */
+
+int
+linux_pkey_alloc_machdep(struct thread *td, uint64_t init_val)
+{
+
+ return (ENOSPC);
+}
+
+int
+linux_pkey_free_machdep(struct thread *td, int pkey)
+{
+
+ return (EINVAL);
+}
+
+int
+linux_pkey_mprotect_machdep(struct thread *td, uintptr_t addr, size_t len,
+ int prot, int pkey)
+{
+
+ return (EINVAL);
+}
diff --git a/sys/arm64/linux/linux_emul_md.h b/sys/arm64/linux/linux_emul_md.h
new file mode 100644
index 000000000000..27424f600f5e
--- /dev/null
+++ b/sys/arm64/linux/linux_emul_md.h
@@ -0,0 +1,18 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _ARM64_LINUX_EMUL_MD_H_
+#define _ARM64_LINUX_EMUL_MD_H_
+
+/*
+ * Machine-dependent part of the Linux process emuldata, embedded in
+ * struct linux_pemuldata as pem_md.
+ */
+struct linux_pemuldata_md {
+ int md_dummy; /* no machine-dependent state yet */
+};
+
+#endif /* !_ARM64_LINUX_EMUL_MD_H_ */
diff --git a/sys/compat/linux/linux_dummy.c b/sys/compat/linux/linux_dummy.c
index 971e070e90aa..dfb1c90e9abf 100644
--- a/sys/compat/linux/linux_dummy.c
+++ b/sys/compat/linux/linux_dummy.c
@@ -110,9 +110,6 @@ DUMMY(mlock2);
DUMMY(preadv2);
DUMMY(pwritev2);
/* Linux 4.8: */
-DUMMY(pkey_mprotect);
-DUMMY(pkey_alloc);
-DUMMY(pkey_free);
/* Linux 4.18: */
DUMMY(io_pgetevents);
/* Linux 5.1: */
diff --git a/sys/compat/linux/linux_emul.c b/sys/compat/linux/linux_emul.c
index e5ab51802468..8ee17e3e484b 100644
--- a/sys/compat/linux/linux_emul.c
+++ b/sys/compat/linux/linux_emul.c
@@ -157,6 +157,7 @@ linux_proc_init(struct thread *td, struct thread *newtd, bool init_thread)
pem = malloc(sizeof(*pem), M_LINUX, M_WAITOK | M_ZERO);
sx_init(&pem->pem_sx, "lpemlk");
+ linux_pemuldata_init_md(td, pem);
p->p_emuldata = pem;
}
newtd->td_emuldata = em;
@@ -183,6 +184,7 @@ linux_proc_init(struct thread *td, struct thread *newtd, bool init_thread)
KASSERT(pem != NULL, ("proc_init: proc emuldata not found.\n"));
pem->persona = 0;
pem->oom_score_adj = 0;
+ linux_pemuldata_exec_md(pem);
}
}
diff --git a/sys/compat/linux/linux_emul.h b/sys/compat/linux/linux_emul.h
index 6dfa31f6edf7..685cbcaa45b4 100644
--- a/sys/compat/linux/linux_emul.h
+++ b/sys/compat/linux/linux_emul.h
@@ -30,6 +30,8 @@
#ifndef _LINUX_EMUL_H_
#define _LINUX_EMUL_H_
+#include <machine/../linux/linux_emul_md.h>
+
struct image_params;
/*
@@ -69,6 +71,7 @@ struct linux_pemuldata {
uint32_t oom_score_adj; /* /proc/self/oom_score_adj */
uint32_t so_timestamp; /* requested timeval */
uint32_t so_timestampns; /* requested timespec */
+ struct linux_pemuldata_md pem_md; /* machine-dependent state */
};
#define LINUX_PEM_XLOCK(p) sx_xlock(&(p)->pem_sx)
@@ -78,4 +81,13 @@ struct linux_pemuldata {
struct linux_pemuldata *pem_find(struct proc *);
+/*
+ * Machine-dependent hooks for struct linux_pemuldata lifecycle
+ * events, implemented per-arch: initialization of pem_md when the
+ * emuldata is created (fork, or a process switching to the Linux
+ * ABI) and reset at exec.
+ */
+void linux_pemuldata_init_md(struct thread *, struct linux_pemuldata *);
+void linux_pemuldata_exec_md(struct linux_pemuldata *);
+
#endif /* !_LINUX_EMUL_H_ */
diff --git a/sys/compat/linux/linux_misc.c b/sys/compat/linux/linux_misc.c
index 3aecc0106aaa..96c0ab81beef 100644
--- a/sys/compat/linux/linux_misc.c
+++ b/sys/compat/linux/linux_misc.c
@@ -359,6 +359,28 @@ linux_mprotect(struct thread *td, struct linux_mprotect_args *uap)
uap->prot));
}
+int
+linux_pkey_mprotect(struct thread *td, struct linux_pkey_mprotect_args *uap)
+{
+
+ return (linux_pkey_mprotect_common(td, uap->start, uap->len,
+ uap->prot, uap->pkey));
+}
+
+int
+linux_pkey_alloc(struct thread *td, struct linux_pkey_alloc_args *uap)
+{
+
+ return (linux_pkey_alloc_common(td, uap->flags, uap->init_val));
+}
+
+int
+linux_pkey_free(struct thread *td, struct linux_pkey_free_args *uap)
+{
+
+ return (linux_pkey_free_common(td, uap->pkey));
+}
+
int
linux_madvise(struct thread *td, struct linux_madvise_args *uap)
{
diff --git a/sys/compat/linux/linux_mmap.c b/sys/compat/linux/linux_mmap.c
index 9fecb6ebb2ad..4fe0df4d5d07 100644
--- a/sys/compat/linux/linux_mmap.c
+++ b/sys/compat/linux/linux_mmap.c
@@ -247,6 +247,44 @@ linux_mprotect_common(struct thread *td, uintptr_t addr, size_t len, int prot)
return (kern_mprotect(td, addr, len, prot, flags));
}
+/*
+ * x86 memory protection keys. The common entry points perform the
+ * parameter validation Linux applies regardless of hardware support,
+ * then defer to the machine-dependent back end.
+ */
+
+int
+linux_pkey_alloc_common(struct thread *td, uint64_t flags, uint64_t init_val)
+{
+
+ if (flags != 0)
+ return (EINVAL);
+ if ((init_val & ~(uint64_t)LINUX_PKEY_ACCESS_MASK) != 0)
+ return (EINVAL);
+ return (linux_pkey_alloc_machdep(td, init_val));
+}
+
+int
+linux_pkey_free_common(struct thread *td, int pkey)
+{
+
+ if (pkey < 0 || pkey >= LINUX_PKEY_MAX)
+ return (EINVAL);
+ return (linux_pkey_free_machdep(td, pkey));
+}
+
+int
+linux_pkey_mprotect_common(struct thread *td, uintptr_t addr, size_t len,
+ int prot, int pkey)
+{
+
+ if (pkey < -1 || pkey >= LINUX_PKEY_MAX)
+ return (EINVAL);
+ if (pkey == -1)
+ return (linux_mprotect_common(td, addr, len, prot));
+ return (linux_pkey_mprotect_machdep(td, addr, len, prot, pkey));
+}
+
/*
* Implement Linux madvise(MADV_DONTNEED), which has unusual semantics: for
* anonymous memory, pages in the range are immediately discarded.
diff --git a/sys/compat/linux/linux_mmap.h b/sys/compat/linux/linux_mmap.h
index 043dec9d40b7..75af79ae1316 100644
--- a/sys/compat/linux/linux_mmap.h
+++ b/sys/compat/linux/linux_mmap.h
@@ -66,6 +66,27 @@
int linux_mmap_common(struct thread *, uintptr_t, size_t, int, int,
int, off_t);
int linux_mprotect_common(struct thread *, uintptr_t, size_t, int);
+int linux_pkey_alloc_common(struct thread *, uint64_t, uint64_t);
+int linux_pkey_free_common(struct thread *, int);
+int linux_pkey_mprotect_common(struct thread *, uintptr_t, size_t, int, int);
+
+/*
+ * Machine-dependent protection key back ends: the real implementation
+ * on amd64 (sys/amd64/linux/linux_pkru.c), stubs behaving as Linux
+ * does on hardware without protection keys elsewhere (each arch's
+ * linux_emul_md.c).
+ */
+int linux_pkey_alloc_machdep(struct thread *, uint64_t);
+int linux_pkey_free_machdep(struct thread *, int);
+int linux_pkey_mprotect_machdep(struct thread *, uintptr_t, size_t, int, int);
+
+/* x86 memory protection keys (pkey_alloc(2) access rights) */
+#define LINUX_PKEY_DISABLE_ACCESS 0x1
+#define LINUX_PKEY_DISABLE_WRITE 0x2
+#define LINUX_PKEY_ACCESS_MASK (LINUX_PKEY_DISABLE_ACCESS | \
+ LINUX_PKEY_DISABLE_WRITE)
+#define LINUX_PKEY_MAX 16 /* keys 0..15; 0 is default */
+
int linux_madvise_common(struct thread *, uintptr_t, size_t, int);
#endif /* _LINUX_MMAP_H_ */
diff --git a/sys/i386/linux/linux_emul_md.c b/sys/i386/linux/linux_emul_md.c
new file mode 100644
index 000000000000..9dd507ad4f49
--- /dev/null
+++ b/sys/i386/linux/linux_emul_md.c
@@ -0,0 +1,52 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/proc.h>
+
+#include <compat/linux/linux_emul.h>
+#include <compat/linux/linux_mmap.h>
+
+/* No machine-dependent emuldata state yet. */
+
+void
+linux_pemuldata_init_md(struct thread *td, struct linux_pemuldata *pem)
+{
+}
+
+void
+linux_pemuldata_exec_md(struct linux_pemuldata *pem)
+{
+}
+
+/*
+ * Protection key back ends: behave as Linux does on hardware without
+ * protection keys. pkey_alloc() reports no free keys and only the
+ * default key semantics remain.
+ */
+
+int
+linux_pkey_alloc_machdep(struct thread *td, uint64_t init_val)
+{
+
+ return (ENOSPC);
+}
+
+int
+linux_pkey_free_machdep(struct thread *td, int pkey)
+{
+
+ return (EINVAL);
+}
+
+int
+linux_pkey_mprotect_machdep(struct thread *td, uintptr_t addr, size_t len,
+ int prot, int pkey)
+{
+
+ return (EINVAL);
+}
diff --git a/sys/i386/linux/linux_emul_md.h b/sys/i386/linux/linux_emul_md.h
new file mode 100644
index 000000000000..ddf5cf183acd
--- /dev/null
+++ b/sys/i386/linux/linux_emul_md.h
@@ -0,0 +1,18 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _I386_LINUX_EMUL_MD_H_
+#define _I386_LINUX_EMUL_MD_H_
+
+/*
+ * Machine-dependent part of the Linux process emuldata, embedded in
+ * struct linux_pemuldata as pem_md.
+ */
+struct linux_pemuldata_md {
+ int md_dummy; /* no machine-dependent state yet */
+};
+
+#endif /* !_I386_LINUX_EMUL_MD_H_ */
diff --git a/sys/modules/linux/Makefile b/sys/modules/linux/Makefile
index 8904e8005416..69b2af142712 100644
--- a/sys/modules/linux/Makefile
+++ b/sys/modules/linux/Makefile
@@ -63,6 +63,7 @@ SRCS+= imgact_linux.c \
linux.c \
linux_dummy.c \
linux_emul.c \
+ linux_emul_md.c \
linux_errno.c \
linux_mib.c \
linux_mmap.c \
diff --git a/sys/modules/linux_common/Makefile b/sys/modules/linux_common/Makefile
index 1fa740b8b823..280d8f8d3ec3 100644
--- a/sys/modules/linux_common/Makefile
+++ b/sys/modules/linux_common/Makefile
@@ -1,6 +1,9 @@
.PATH: ${SRCTOP}/sys/compat/linux
.if ${MACHINE_CPUARCH} == "amd64"
-.PATH: ${SRCTOP}/sys/x86/linux
+.PATH: ${SRCTOP}/sys/amd64/linux ${SRCTOP}/sys/x86/linux
+.endif
+.if ${MACHINE_CPUARCH} == "aarch64"
+.PATH: ${SRCTOP}/sys/arm64/linux
.endif
KMOD= linux_common
@@ -9,7 +12,10 @@ SRCS= linux_common.c linux_mib.c linux_mmap.c linux_util.c linux_emul.c \
linux.c device_if.h vnode_if.h bus_if.h opt_inet6.h opt_inet.h
.if ${MACHINE_CPUARCH} == "amd64"
-SRCS+= linux_x86.c linux_vdso_selector_x86.c
+SRCS+= linux_pkru.c linux_x86.c linux_vdso_selector_x86.c
+.endif
+.if ${MACHINE_CPUARCH} == "aarch64"
+SRCS+= linux_emul_md.c
.endif
EXPORT_SYMS=
diff --git a/sys/x86/include/sysarch.h b/sys/x86/include/sysarch.h
index cf7ee9ee5518..9a8c95d9ae75 100644
--- a/sys/x86/include/sysarch.h
+++ b/sys/x86/include/sysarch.h
@@ -164,6 +164,7 @@ int amd64_set_ldt(struct thread *, struct i386_ldt_args *,
struct user_segment_descriptor *);
int amd64_get_ioperm(struct thread *, struct i386_ioperm_args *);
int amd64_set_ioperm(struct thread *, struct i386_ioperm_args *);
+int amd64_pkru_update(struct thread *, uintptr_t, size_t, u_int, int, bool);
#endif
#endif /* !_MACHINE_SYSARCH_H_ */