// SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (c) 2017 Google, Inc. */ /* * Regression test for commit 814fb7bb7db5 ("x86/fpu: Don't let userspace set * bogus xcomp_bv"), or CVE-2017-15537. This bug allowed ptrace(pid, * PTRACE_SETREGSET, NT_X86_XSTATE, &iov) to assign a task an invalid FPU state * --- specifically, by setting reserved bits in xstate_header.xcomp_bv. This * made restoring the FPU registers fail when switching to the task, causing the * FPU registers to take on the values from other tasks. * * To detect the bug, we have a subprocess run a loop checking its xmm0 register * for corruption. This detects the case where the FPU state became invalid and * the kernel is not restoring the process's registers. Note that we have to * set the expected value of xmm0 to all 0's since it is acceptable behavior for * the kernel to simply reinitialize the FPU state upon seeing that it is * invalid. To increase the chance of detecting the problem, we also create * additional subprocesses that spin with different xmm0 contents. * * Thus bug affected the x86 architecture only. Other architectures could have * similar bugs as well, but this test has to be x86-specific because it has to * know about the architecture-dependent FPU state. */ #include #include #include #include #include #include #include #include "config.h" #include "ptrace.h" #include "tst_test.h" #ifndef PTRACE_GETREGSET # define PTRACE_GETREGSET 0x4204 #endif #ifndef PTRACE_SETREGSET # define PTRACE_SETREGSET 0x4205 #endif #ifndef NT_X86_XSTATE # define NT_X86_XSTATE 0x202 #endif #ifdef __x86_64__ static void check_regs_loop(uint32_t initval) { const unsigned long num_iters = 1000000000; uint32_t xmm0[4] = { initval, initval, initval, initval }; int status = 1; asm volatile(" movdqu %0, %%xmm0\n" " mov %0, %%rbx\n" "1: dec %2\n" " jz 2f\n" " movdqu %%xmm0, %0\n" " mov %0, %%rax\n" " cmp %%rax, %%rbx\n" " je 1b\n" " jmp 3f\n" "2: mov $0, %1\n" "3:\n" : "+m" (xmm0), "+r" (status) : "r" (num_iters) : "rax", "rbx", "xmm0"); if (status) { tst_res(TFAIL, "xmm registers corrupted! initval=%08X, xmm0=%08X%08X%08X%08X\n", initval, xmm0[0], xmm0[1], xmm0[2], xmm0[3]); } exit(status); } static void do_test(void) { int i; int num_cpus = tst_ncpus(); pid_t pid; uint64_t xstate[512]; struct iovec iov = { .iov_base = xstate, .iov_len = sizeof(xstate) }; int status; bool okay; pid = SAFE_FORK(); if (pid == 0) { TST_CHECKPOINT_WAKE(0); check_regs_loop(0x00000000); } for (i = 0; i < num_cpus; i++) { if (SAFE_FORK() == 0) check_regs_loop(0xDEADBEEF); } TST_CHECKPOINT_WAIT(0); sched_yield(); TEST(ptrace(PTRACE_ATTACH, pid, 0, 0)); if (TST_RET != 0) tst_brk(TBROK | TTERRNO, "PTRACE_ATTACH failed"); SAFE_WAITPID(pid, NULL, 0); TEST(ptrace(PTRACE_GETREGSET, pid, NT_X86_XSTATE, &iov)); if (TST_RET != 0) { if (TST_ERR == EIO) tst_brk(TCONF, "GETREGSET/SETREGSET is unsupported"); if (TST_ERR == EINVAL) tst_brk(TCONF, "NT_X86_XSTATE is unsupported"); if (TST_ERR == ENODEV) tst_brk(TCONF, "CPU doesn't support XSAVE instruction"); tst_brk(TBROK | TTERRNO, "PTRACE_GETREGSET failed with unexpected error"); } xstate[65] = -1; /* sets all bits in xstate_header.xcomp_bv */ /* * Old kernels simply masked out all the reserved bits in the xstate * header (causing the PTRACE_SETREGSET command here to succeed), while * new kernels will reject them (causing the PTRACE_SETREGSET command * here to fail with EINVAL). We accept either behavior, as neither * behavior reliably tells us whether the real bug (which we test for * below in either case) is present. */ TEST(ptrace(PTRACE_SETREGSET, pid, NT_X86_XSTATE, &iov)); if (TST_RET == 0) { tst_res(TINFO, "PTRACE_SETREGSET with reserved bits succeeded"); } else if (TST_ERR == EINVAL) { tst_res(TINFO, "PTRACE_SETREGSET with reserved bits failed with EINVAL"); } else { tst_brk(TBROK | TTERRNO, "PTRACE_SETREGSET failed with unexpected error"); } /* * It is possible for test child 'pid' to crash on AMD * systems (e.g. AMD Opteron(TM) Processor 6234) with * older kernels. This causes tracee to stop and sleep * in ptrace_stop(). Without resuming the tracee, the * test hangs at do_test()->tst_reap_children() called * by the library. Use detach here, so we don't need to * worry about potential stops after this point. */ TEST(ptrace(PTRACE_DETACH, pid, 0, 0)); if (TST_RET != 0) tst_brk(TBROK | TTERRNO, "PTRACE_DETACH failed"); /* If child 'pid' crashes, only report it as info. */ SAFE_WAITPID(pid, &status, 0); if (WIFEXITED(status)) { tst_res(TINFO, "test child %d exited, retcode: %d", pid, WEXITSTATUS(status)); } if (WIFSIGNALED(status)) { tst_res(TINFO, "test child %d exited, termsig: %d", pid, WTERMSIG(status)); } okay = true; for (i = 0; i < num_cpus; i++) { SAFE_WAIT(&status); okay &= (WIFEXITED(status) && WEXITSTATUS(status) == 0); } if (okay) tst_res(TPASS, "wasn't able to set invalid FPU state"); } static struct tst_test test = { .test_all = do_test, .forks_child = 1, .needs_checkpoints = 1, .tags = (const struct tst_tag[]) { {"linux-git", "814fb7bb7db5"}, {"CVE", "2017-15537"}, {} } }; #else /* !__x86_64__ */ TST_TEST_TCONF("this test is only supported on x86_64"); #endif /* __x86_64__ */