Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F167285530
D58782.id183906.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
16 KB
Referenced Files
None
Subscribers
None
D58782.id183906.diff
View Options
diff --git a/sys/amd64/amd64/sys_machdep.c b/sys/amd64/amd64/sys_machdep.c
--- a/sys/amd64/amd64/sys_machdep.c
+++ b/sys/amd64/amd64/sys_machdep.c
@@ -167,11 +167,44 @@
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 @@
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_pkru.c b/sys/amd64/linux/linux_pkru.c
new file mode 100644
--- /dev/null
+++ b/sys/amd64/linux/linux_pkru.c
@@ -0,0 +1,209 @@
+/*
+ * 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);
+}
+
+int
+linux_pkey_alloc_common(struct thread *td, uint64_t flags, uint64_t init_val)
+{
+ struct linux_pemuldata *pem;
+ uint32_t free_keys;
+ int key;
+
+ if (flags != 0)
+ return (EINVAL);
+ if ((init_val & ~(uint64_t)LINUX_PKEY_ACCESS_MASK) != 0)
+ return (EINVAL);
+ if (!linux_pkey_supported())
+ return (ENOSPC);
+
+ pem = pem_find(td->td_proc);
+ LINUX_PEM_XLOCK(pem);
+ free_keys = ~pem->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->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_common(struct thread *td, int pkey)
+{
+ struct linux_pemuldata *pem;
+
+ if (pkey < 0 || pkey >= LINUX_PKEY_MAX)
+ return (EINVAL);
+ if (!linux_pkey_supported())
+ return (EINVAL);
+
+ pem = pem_find(td->td_proc);
+ LINUX_PEM_XLOCK(pem);
+ if ((pem->pkey_allocation_map & (1u << pkey)) == 0) {
+ LINUX_PEM_XUNLOCK(pem);
+ return (EINVAL);
+ }
+ pem->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_common(struct thread *td, uintptr_t addr, size_t len,
+ int prot, int pkey)
+{
+ struct linux_pemuldata *pem;
+ int error;
+
+ if (pkey < -1 || pkey >= LINUX_PKEY_MAX)
+ return (EINVAL);
+ if (pkey == -1)
+ return (linux_mprotect_common(td, addr, len, prot));
+
+ if (!linux_pkey_supported())
+ return (EINVAL);
+
+ pem = pem_find(td->td_proc);
+ LINUX_PEM_SLOCK(pem);
+ if ((pem->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
--- a/sys/amd64/linux/linux_sysvec.c
+++ b/sys/amd64/linux/linux_sysvec.c
@@ -64,6 +64,7 @@
#include <compat/linux/linux_ioctl.h>
#include <compat/linux/linux_mib.h>
#include <compat/linux/linux_misc.h>
+#include <compat/linux/linux_mmap.h>
#include <compat/linux/linux_signal.h>
#include <compat/linux/linux_util.h>
#include <compat/linux/linux_vdso.h>
@@ -271,6 +272,9 @@
* 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
--- a/sys/amd64/linux32/linux32_sysvec.c
+++ b/sys/amd64/linux32/linux32_sysvec.c
@@ -69,6 +69,7 @@
#include <compat/linux/linux_ioctl.h>
#include <compat/linux/linux_mib.h>
#include <compat/linux/linux_misc.h>
+#include <compat/linux/linux_mmap.h>
#include <compat/linux/linux_signal.h>
#include <compat/linux/linux_util.h>
#include <compat/linux/linux_vdso.h>
@@ -609,6 +610,9 @@
x86_clear_dbregs(pcb);
fpstate_drop(td);
+
+ /* Linux processes start with PKRU denying unallocated keys. */
+ linux_pkru_exec_init(td);
}
/*
diff --git a/sys/compat/linux/linux_dummy.c b/sys/compat/linux/linux_dummy.c
--- a/sys/compat/linux/linux_dummy.c
+++ b/sys/compat/linux/linux_dummy.c
@@ -110,9 +110,6 @@
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.h b/sys/compat/linux/linux_emul.h
--- a/sys/compat/linux/linux_emul.h
+++ b/sys/compat/linux/linux_emul.h
@@ -69,8 +69,16 @@
uint32_t oom_score_adj; /* /proc/self/oom_score_adj */
uint32_t so_timestamp; /* requested timeval */
uint32_t so_timestampns; /* requested timespec */
+ uint32_t 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
+
#define LINUX_PEM_XLOCK(p) sx_xlock(&(p)->pem_sx)
#define LINUX_PEM_XUNLOCK(p) sx_xunlock(&(p)->pem_sx)
#define LINUX_PEM_SLOCK(p) sx_slock(&(p)->pem_sx)
diff --git a/sys/compat/linux/linux_emul.c b/sys/compat/linux/linux_emul.c
--- a/sys/compat/linux/linux_emul.c
+++ b/sys/compat/linux/linux_emul.c
@@ -137,7 +137,7 @@
linux_proc_init(struct thread *td, struct thread *newtd, bool init_thread)
{
struct linux_emuldata *em;
- struct linux_pemuldata *pem;
+ struct linux_pemuldata *pem, *ppem;
struct proc *p;
if (newtd != NULL) {
@@ -157,6 +157,25 @@
pem = malloc(sizeof(*pem), M_LINUX, M_WAITOK | M_ZERO);
sx_init(&pem->pem_sx, "lpemlk");
+
+ /*
+ * 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.
+ */
+ ppem = pem_find(td->td_proc);
+ if (ppem != NULL)
+ pem->pkey_allocation_map =
+ ppem->pkey_allocation_map;
+ else
+ pem->pkey_allocation_map =
+ LINUX_PKEY_INITIAL_MAP;
p->p_emuldata = pem;
}
newtd->td_emuldata = em;
@@ -183,6 +202,7 @@
KASSERT(pem != NULL, ("proc_init: proc emuldata not found.\n"));
pem->persona = 0;
pem->oom_score_adj = 0;
+ pem->pkey_allocation_map = LINUX_PKEY_INITIAL_MAP;
}
}
diff --git a/sys/compat/linux/linux_misc.c b/sys/compat/linux/linux_misc.c
--- a/sys/compat/linux/linux_misc.c
+++ b/sys/compat/linux/linux_misc.c
@@ -359,6 +359,28 @@
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.h b/sys/compat/linux/linux_mmap.h
--- a/sys/compat/linux/linux_mmap.h
+++ b/sys/compat/linux/linux_mmap.h
@@ -66,6 +66,25 @@
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);
+
+/* 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 */
+
+#if defined(__amd64__)
+/*
+ * Initial PKRU at exec: access disabled for keys 1..15, key 0 open;
+ * the Linux init_pkru default.
+ */
+#define LINUX_PKRU_INIT 0x55555554
+void linux_pkru_exec_init(struct thread *);
+#endif
int linux_madvise_common(struct thread *, uintptr_t, size_t, int);
#endif /* _LINUX_MMAP_H_ */
diff --git a/sys/compat/linux/linux_mmap.c b/sys/compat/linux/linux_mmap.c
--- a/sys/compat/linux/linux_mmap.c
+++ b/sys/compat/linux/linux_mmap.c
@@ -247,6 +247,44 @@
return (kern_mprotect(td, addr, len, prot, flags));
}
+#if !defined(__amd64__)
+/*
+ * The x86 protection key implementation lives in
+ * sys/amd64/linux/linux_pkru.c. Elsewhere, behave as Linux does on
+ * hardware without protection keys: arguments are validated in the
+ * same order, then pkey_alloc() reports no free keys and only the
+ * default key semantics remain.
+ */
+
+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 (ENOSPC);
+}
+
+int
+linux_pkey_free_common(struct thread *td, int pkey)
+{
+
+ return (EINVAL);
+}
+
+int
+linux_pkey_mprotect_common(struct thread *td, uintptr_t addr, size_t len,
+ int prot, int pkey)
+{
+
+ if (pkey == -1)
+ return (linux_mprotect_common(td, addr, len, prot));
+ return (EINVAL);
+}
+#endif /* !__amd64__ */
+
/*
* Implement Linux madvise(MADV_DONTNEED), which has unusual semantics: for
* anonymous memory, pages in the range are immediately discarded.
diff --git a/sys/modules/linux_common/Makefile b/sys/modules/linux_common/Makefile
--- a/sys/modules/linux_common/Makefile
+++ b/sys/modules/linux_common/Makefile
@@ -1,6 +1,6 @@
.PATH: ${SRCTOP}/sys/compat/linux
.if ${MACHINE_CPUARCH} == "amd64"
-.PATH: ${SRCTOP}/sys/x86/linux
+.PATH: ${SRCTOP}/sys/amd64/linux ${SRCTOP}/sys/x86/linux
.endif
KMOD= linux_common
@@ -9,7 +9,7 @@
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
EXPORT_SYMS=
diff --git a/sys/x86/include/sysarch.h b/sys/x86/include/sysarch.h
--- a/sys/x86/include/sysarch.h
+++ b/sys/x86/include/sysarch.h
@@ -164,6 +164,7 @@
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_ */
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Aug 21, 4:00 PM (13 h, 42 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
36992601
Default Alt Text
D58782.id183906.diff (16 KB)
Attached To
Mode
D58782: linux: implement pkey_alloc, pkey_free and pkey_mprotect
Attached
Detach File
Event Timeline
Log In to Comment