diff options
Diffstat (limited to 'sys/amd64/linux')
| -rw-r--r-- | sys/amd64/linux/linux_emul_md.h | 35 | ||||
| -rw-r--r-- | sys/amd64/linux/linux_pkru.c | 226 | ||||
| -rw-r--r-- | sys/amd64/linux/linux_sysvec.c | 4 |
3 files changed, 265 insertions, 0 deletions
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 |
