blob: 362cee5434f8c461065887f0057243ea041bb112 [file] [log] [blame]
// 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 "tst_test.h"
#ifdef __x86_64__
#include <errno.h>
#include <inttypes.h>
#include <sched.h>
#include <stdbool.h>
#include <stdlib.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include "config.h"
#include "ptrace.h"
#include "tst_safe_macros.h"
#include "lapi/cpuid.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
#ifndef CPUID_LEAF_XSTATE
# define CPUID_LEAF_XSTATE 0xd
#endif
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;
uint32_t eax = 0, ebx = 0, ecx = 0, edx = 0;
uint64_t *xstate;
/*
* CPUID.(EAX=0DH, ECX=0H):EBX: maximum size (bytes, from the beginning
* of the XSAVE/XRSTOR save area) required by enabled features in XCR0.
*/
__cpuid_count(CPUID_LEAF_XSTATE, ecx, eax, ebx, ecx, edx);
xstate = SAFE_MEMALIGN(64, ebx);
struct iovec iov = { .iov_base = xstate, .iov_len = ebx };
int status;
bool okay;
tst_res(TINFO, "CPUID.(EAX=%u, ECX=0):EAX=%u, EBX=%u, ECX=%u, EDX=%u",
CPUID_LEAF_XSTATE, eax, ebx, ecx, edx);
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) {
free(xstate);
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) {
free(xstate);
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 {
free(xstate);
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) {
free(xstate);
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");
free(xstate);
}
static struct tst_test test = {
.test_all = do_test,
.forks_child = 1,
.needs_checkpoints = 1,
.supported_archs = (const char *const []) {
"x86_64",
NULL
},
.tags = (const struct tst_tag[]) {
{"linux-git", "814fb7bb7db5"},
{"CVE", "2017-15537"},
{}
}
};
#else
TST_TEST_TCONF("Tests an x86_64 feature");
#endif /* if x86 */