blob: b96c23337cdcc28543cb6d5915ae297ee0f58a41 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef __x86_64__
// The ZX_PROP_REGISTER_FS and ZX_PROP_REGISTER_GS properties exist as
// stand-ins for the rdfsbase/wrfsbase and rdgsbase/rdgsbase instructions
// available on newer x86 processors. So this test ensures that the system
// calls behave consistently with the machine instructions.
// This test exercises the CPU instructions as well as the system calls, so
// it can verify that they interact consistently. To test both with and
// without the CPU instructions available, QEMU (with or without KVM) can
// be passed the `-cpu -fsgsbase` switch when running on hardware that does
// actually support it to emulate hardware that does not.
#include <cpuid.h>
#include <lib/arch/x86/cpuid.h>
#include <lib/elfldltl/machine.h>
#include <lib/fit/defer.h>
#include <lib/zx/exception.h>
#include <lib/zx/thread.h>
#include <zircon/syscalls.h>
#include <zircon/syscalls/debug.h>
#include <zircon/syscalls/exception.h>
#include <zircon/syscalls/object.h>
#include <zircon/threads.h>
#include <atomic>
#include <cstdint>
#include <mutex>
#include <string_view>
#include <thread>
#include <zxtest/zxtest.h>
namespace {
constexpr std::string_view kThreadName = "fsgsbase-test-thread";
// These values meet the "canonical address" test but are certain never to be
// valid user addresses.
constexpr uint64_t kInitialValue = 0xdead1;
constexpr uint64_t kWriteInsnValue = 0xdead2;
// This is a canonical address, but never a valid user address.
constexpr uint64_t kNonuserValue = -4096;
constexpr uint64_t kNoncanonicalValue = 0x1010101010101010;
constexpr uint64_t kUndefinedInsnValue = 0xfed1bad1;
constexpr uint64_t kDatumValue = 0x1234567890abcdef;
// When the {rd,wr}{fs,gs}base instructions are not available, this is the
// Zircon exception that will be generated for illegal instruction traps.
constexpr uint32_t kNoInsnsException = ZX_EXCP_UNDEFINED_INSTRUCTION;
// This is the Zircon exception that will be generated for general protection
// faults produced by use of noncanonical addresses.
constexpr uint32_t kNoncanonicalException = ZX_EXCP_GENERAL;
// Most of the test cases work by launching a separate thread that will attempt
// to use the %fs.base and %gs.base features directly. Since %fs.base is used
// as the thread pointer in the normal Fuchsia compiler ABI, the code for this
// thread disables all compiler features that depend on it (e.g. SafeStack) and
// avoids standard runtime code that uses the full ABI.
#ifdef __clang__
#define BARE_FUNCTION [[clang::no_sanitize("all")]]
#else
#define BARE_FUNCTION
#endif
// When rdfsbase/rdgsbase are not available, there is no way for user code to
// retrieve the %fs.base and %gs.base values directly from the CPU. Only
// memory accesses using them in the effective address calculation can be used.
BARE_FUNCTION uint64_t LoadFromFs() {
uint64_t value;
__asm__("mov %%fs:0, %0" : "=r"(value));
return value;
}
BARE_FUNCTION uint64_t LoadFromGs() {
uint64_t value;
__asm__("mov %%gs:0, %0" : "=r"(value));
return value;
}
// These are defined in assembly so we can know the instruction's exact PC when
// examining thread register state in the exception handler. Each entry point
// is the instruction itself, and the *End symbol is just after it. So the
// exception handler will move the PC ahead to skip the instruction after it
// faults, and then change the %rax register value to synthesize its result.
extern "C" uint64_t RdFsBase(), RdGsBase();
extern "C" void WrFsBase(uint64_t), WrGsBase(uint64_t);
extern "C" void RdFsBaseEnd(), RdGsBaseEnd(), WrFsBaseEnd(), WrGsBaseEnd();
__asm__(
R"""(
.pushsection .text.RdFsBase, "a", %progbits
.type RdFsBase, %function
RdFsBase:
.cfi_startproc
rdfsbase %rax
RdFsBaseEnd:
ret
.cfi_endproc
.size RdFsBase, . - RdFsBase
.popsection
.pushsection .text.RdGsBase, "a", %progbits
.type RdGsBase, %function
RdGsBase:
.cfi_startproc
rdgsbase %rax
RdGsBaseEnd:
ret
.cfi_endproc
.size RdGsBase, . - RdGsBase
.popsection
.pushsection .text.WrFsBase, "a", %progbits
.type WrFsBase, %function
WrFsBase:
.cfi_startproc
wrfsbase %rdi
WrFsBaseEnd:
ret
.cfi_endproc
.size WrFsBase, . - WrFsBase
.popsection
.pushsection .text.WrGsBase, "a", %progbits
.type WrGsBase, %function
WrGsBase:
.cfi_startproc
wrgsbase %rdi
WrGsBaseEnd:
ret
.cfi_endproc
.size WrGsBase, . - WrGsBase
.popsection
)""");
// The tests are exactly the same for the two registers, so everything is
// parameterized by the WhichRegister data structure.
struct WhichRegister {
uint64_t zx_thread_state_general_regs_t::*member;
uint32_t property;
void (*write_insn)(uint64_t);
void (*after_write_insn)();
uint64_t (*read_insn)();
void (*after_read_insn)();
uint64_t (*load_from)();
};
constexpr WhichRegister kFs = {
.member = &zx_thread_state_general_regs_t::fs_base,
.property = ZX_PROP_REGISTER_FS,
.write_insn = WrFsBase,
.after_write_insn = WrFsBaseEnd,
.read_insn = RdFsBase,
.after_read_insn = RdFsBaseEnd,
.load_from = LoadFromFs,
};
constexpr WhichRegister kGs = {
.member = &zx_thread_state_general_regs_t::gs_base,
.property = ZX_PROP_REGISTER_GS,
.write_insn = WrGsBase,
.after_write_insn = WrGsBaseEnd,
.read_insn = RdGsBase,
.after_read_insn = RdGsBaseEnd,
.load_from = LoadFromGs,
};
// Some x86 CPUs support the instructions and some don't. This test should
// work correctly in either case.
bool HaveInsns() {
static bool have_insns = []() -> bool {
arch::CpuidIo cpu;
__cpuid_count(arch::CpuidExtendedFeatureFlagsB::kLeaf,
arch::CpuidExtendedFeatureFlagsB::kSubleaf, cpu.values_[arch::CpuidIo::kEax],
cpu.values_[arch::CpuidIo::kEbx], cpu.values_[arch::CpuidIo::kEcx],
cpu.values_[arch::CpuidIo::kEdx]);
return arch::CpuidExtendedFeatureFlagsB::Get().ReadFrom(&cpu).fsgsbase();
}();
return have_insns;
}
// This is all the state communicated between the little test thread and the
// controlling test code. The TestFsGsThread function just follows these
// instructions in a rote fashion. The controlling test expects different
// results (values delivered and/or faults triggered) based on the values used
// and on whether the CPU supports the instructions.
struct TestData {
uint64_t write_insn = -1; // Write this value using the write_insn function.
uint64_t write_prop = -1; // Write this value using the system call.
uint64_t read_insn = -1; // Collect the value read using read_insn.
uint64_t read_prop = -1; // Collect the value read using the system call.
uint64_t load = -1; // Collect the value using the load_from function.
zx_handle_t thread; // Thread-self handle for use in the system call.
// Results of the system call attempts.
zx_status_t get_status = ZX_ERR_BAD_STATE;
zx_status_t set_status = ZX_ERR_BAD_STATE;
};
// This runs in a raw thread with minimal stack and avoids anything that wants
// to use the thread register, since that's %fs.base.
BARE_FUNCTION [[noreturn]] void TestFsGsThread(const WhichRegister& reg, TestData& test) {
// Read the old value via the instruction.
test.read_insn = reg.read_insn();
// Write the new value via the instruction.
reg.write_insn(test.write_insn);
// Read the value via the system call.
test.get_status =
zx_object_get_property(test.thread, reg.property, &test.read_prop, sizeof(test.read_prop));
// Write the value via the system call.
test.set_status =
zx_object_set_property(test.thread, reg.property, &test.write_prop, sizeof(test.write_prop));
// Load from the value as a pointer via the addressing prefix.
test.load = reg.load_from();
// Synchronize all stores with the waiting thread.
std::atomic_thread_fence(std::memory_order_seq_cst);
// All done.
zx_thread_exit();
}
// A handy type for C++ new[] to deliver a correctly-aligned stack allocation.
struct alignas(16) StackAligned {
std::byte bytes[16];
};
void TestInThread(const WhichRegister& reg, TestData& test, uint32_t expected_read_exception,
uint32_t expected_write_exception, uintptr_t expected_fault) {
// Create a new raw thread and give it a small stack.
std::unique_ptr<StackAligned[]> thread_stack(
new StackAligned[zx_system_get_page_size() / sizeof(StackAligned)]);
zx::thread thread;
ASSERT_OK(
zx::thread::create(*zx::process::self(), kThreadName.data(), kThreadName.size(), 0, &thread));
// Store the thread's own handle so it can make the system calls.
test.thread = thread.get();
// Synchronize all stores before the new thread reads them.
std::atomic_thread_fence(std::memory_order_seq_cst);
// Set up to receive the new thread's exceptions.
zx::channel exc_channel;
ASSERT_OK(thread.create_exception_channel(0, &exc_channel));
// Start the thread suspended so its register state can be modified.
zx::suspend_token suspended;
ASSERT_OK(thread.suspend(&suspended));
// The thread will start with the call TestFsGsThread(reg, test).
const uintptr_t sp = elfldltl::AbiTraits<>::InitialStackPointer<uintptr_t>(
reinterpret_cast<uintptr_t>(thread_stack.get()), zx_system_get_page_size());
const uintptr_t pc = reinterpret_cast<uintptr_t>(&TestFsGsThread);
const uintptr_t arg1 = reinterpret_cast<uintptr_t>(&reg);
const uintptr_t arg2 = reinterpret_cast<uintptr_t>(&test);
ASSERT_OK(thread.start(pc, sp, arg1, arg2));
// The thread was "started suspended", but that means it starts up and then
// suspends, so it has to be synchronized.
zx_signals_t signals = ZX_THREAD_SUSPENDED;
ASSERT_OK(thread.wait_one(signals, zx::time::infinite(), &signals));
ASSERT_TRUE(signals & ZX_THREAD_SUSPENDED);
// Now it's possible examine and mutate the initial register state.
zx_thread_state_general_regs_t regs;
ASSERT_OK(thread.read_state(ZX_THREAD_STATE_GENERAL_REGS, &regs, sizeof(regs)));
EXPECT_EQ(pc, regs.rip);
EXPECT_EQ(sp, regs.rsp);
EXPECT_EQ(arg1, regs.rdi);
EXPECT_EQ(arg2, regs.rsi);
EXPECT_EQ(arg2, regs.rsi);
// Set the register's initial value on thread start.
regs.*reg.member = kInitialValue;
ASSERT_OK(thread.write_state(ZX_THREAD_STATE_GENERAL_REGS, &regs, sizeof(regs)));
// Now let the thread run.
suspended.reset();
// Wait for an exception message and/or thread death.
zx_wait_item_t wait_items[] = {
{.handle = exc_channel.get(), .waitfor = ZX_CHANNEL_READABLE},
{.handle = thread.get(), .waitfor = ZX_THREAD_TERMINATED},
};
const zx_wait_item_t& wait_channel = wait_items[0];
const zx_wait_item_t& wait_thread = wait_items[1];
ASSERT_OK(zx::handle::wait_many(wait_items, std::size(wait_items), zx::time::infinite()));
// If the rd*base instruction is expected to fault, catch that fault.
if (expected_read_exception) {
ASSERT_TRUE(wait_channel.pending & ZX_CHANNEL_READABLE);
ASSERT_FALSE(wait_thread.pending & ZX_THREAD_TERMINATED);
// Read the exception message.
zx::exception exc;
zx_exception_info_t exc_info;
uint32_t nbytes = 0, nhandles = 0;
ASSERT_OK(exc_channel.read(0, &exc_info, exc.reset_and_get_address(), sizeof(exc_info), 1,
&nbytes, &nhandles));
ASSERT_EQ(sizeof(exc_info), nbytes);
ASSERT_EQ(1u, nhandles);
// Verify it was the expected fault at the expected PC.
EXPECT_EQ(expected_read_exception, exc_info.type);
zx_thread_state_general_regs_t regs;
ASSERT_OK(thread.read_state(ZX_THREAD_STATE_GENERAL_REGS, &regs, sizeof(regs)));
EXPECT_EQ(reinterpret_cast<uintptr_t>(reg.read_insn), regs.rip);
// Warp the PC past the instruction and set the return-value register.
regs.rip = reinterpret_cast<uintptr_t>(reg.after_read_insn);
regs.rax = kUndefinedInsnValue;
ASSERT_OK(thread.write_state(ZX_THREAD_STATE_GENERAL_REGS, &regs, sizeof(regs)));
// Let the thread resume from the exception at the new PC.
constexpr uint32_t kExceptionState = ZX_EXCEPTION_STATE_HANDLED;
ASSERT_OK(exc.set_property(ZX_PROP_EXCEPTION_STATE, &kExceptionState, sizeof(kExceptionState)));
exc.reset();
// Wait for the next fault or completion.
ASSERT_OK(zx::handle::wait_many(wait_items, std::size(wait_items), zx::time::infinite()));
}
// If the wr*base instruction is expected to fault, catch that fault.
if (expected_write_exception) {
ASSERT_TRUE(wait_channel.pending & ZX_CHANNEL_READABLE);
ASSERT_FALSE(wait_thread.pending & ZX_THREAD_TERMINATED);
// Read the exception message.
zx::exception exc;
zx_exception_info_t exc_info;
uint32_t nbytes = 0, nhandles = 0;
ASSERT_OK(exc_channel.read(0, &exc_info, exc.reset_and_get_address(), sizeof(exc_info), 1,
&nbytes, &nhandles));
ASSERT_EQ(sizeof(exc_info), nbytes);
ASSERT_EQ(1u, nhandles);
// Verify it was the expected fault at the expected PC.
EXPECT_EQ(expected_write_exception, exc_info.type);
zx_thread_state_general_regs_t regs;
ASSERT_OK(thread.read_state(ZX_THREAD_STATE_GENERAL_REGS, &regs, sizeof(regs)));
EXPECT_EQ(reinterpret_cast<uintptr_t>(reg.write_insn), regs.rip);
// Warp the PC past the instruction so the thread can continue. The
// write_insn function returns void, so regs.rax doesn't matter here.
regs.rip = reinterpret_cast<uintptr_t>(reg.after_write_insn);
ASSERT_OK(thread.write_state(ZX_THREAD_STATE_GENERAL_REGS, &regs, sizeof(regs)));
// Let the thread resume from the exception at the new PC.
constexpr uint32_t kExceptionState = ZX_EXCEPTION_STATE_HANDLED;
ASSERT_OK(exc.set_property(ZX_PROP_EXCEPTION_STATE, &kExceptionState, sizeof(kExceptionState)));
exc.reset();
// Wait for the next fault or completion.
ASSERT_OK(zx::handle::wait_many(wait_items, std::size(wait_items), zx::time::infinite()));
}
// If the load was expected to fault on a bad address, catch that fault.
if (expected_fault) {
ASSERT_TRUE(wait_channel.pending & ZX_CHANNEL_READABLE);
ASSERT_FALSE(wait_thread.pending & ZX_THREAD_TERMINATED);
// Read the exception message.
zx::exception exc;
zx_exception_info_t exc_info;
uint32_t nbytes = 0, nhandles = 0;
ASSERT_OK(exc_channel.read(0, &exc_info, exc.reset_and_get_address(), sizeof(exc_info), 1,
&nbytes, &nhandles));
ASSERT_EQ(sizeof(exc_info), nbytes);
ASSERT_EQ(1u, nhandles);
// Verify it was the expected fault at the expected fault address. We
// don't bother to check for precise PC here, since we don't really
// need to mutate the register state and resume as in the cases above.
EXPECT_EQ(ZX_EXCP_FATAL_PAGE_FAULT, exc_info.type);
zx_exception_report_t report;
ASSERT_OK(thread.get_info(ZX_INFO_THREAD_EXCEPTION_REPORT, &report, sizeof(report), nullptr,
nullptr));
EXPECT_EQ(ZX_EXCP_FATAL_PAGE_FAULT, report.header.type);
EXPECT_EQ(expected_fault, report.context.arch.u.x86_64.cr2);
// Force the thread to exit when it resumes.
constexpr uint32_t kExceptionState = ZX_EXCEPTION_STATE_THREAD_EXIT;
ASSERT_OK(exc.set_property(ZX_PROP_EXCEPTION_STATE, &kExceptionState, sizeof(kExceptionState)));
// Let the thread resume and exit.
exc.reset();
// Wait for termination.
ASSERT_OK(zx::handle::wait_many(wait_items, std::size(wait_items), zx::time::infinite()));
}
// All expected faults (if any) should have been handled above.
// So the thread should have terminated.
EXPECT_TRUE(wait_thread.pending & ZX_THREAD_TERMINATED);
EXPECT_FALSE(wait_channel.pending & ZX_CHANNEL_READABLE);
}
// The valid case has no faults unless the instructions are not supported.
void TestValid(const WhichRegister& reg) {
const bool have_insns = HaveInsns();
const uint32_t insn_except = have_insns ? 0 : kNoInsnsException;
uint64_t datum = kDatumValue;
const uintptr_t datum_address = reinterpret_cast<uintptr_t>(&datum);
TestData test = {
.write_insn = kWriteInsnValue,
.write_prop = datum_address,
};
ASSERT_NO_FATAL_FAILURE(TestInThread(reg, test, insn_except, insn_except, 0));
EXPECT_OK(test.get_status);
EXPECT_OK(test.set_status);
if (have_insns) {
EXPECT_EQ(kInitialValue, test.read_insn);
EXPECT_EQ(kWriteInsnValue, test.read_prop);
} else {
EXPECT_EQ(kUndefinedInsnValue, test.read_insn);
EXPECT_EQ(kInitialValue, test.read_prop);
}
EXPECT_EQ(datum, test.load);
}
TEST(Property, RegisterFs) { ASSERT_NO_FATAL_FAILURE(TestValid(kFs)); }
TEST(Property, RegisterGs) { ASSERT_NO_FATAL_FAILURE(TestValid(kGs)); }
// This case is just as valid but is using a value that's a bad address.
// So the only thing that should be different is the final load, that
// faults with the expected address instead of succeeding.
void TestFault(const WhichRegister& reg) {
// Allocate a VMAR to get some known-valid user address space that is
// sure to be inaccessible.
zx::vmar vmar;
uintptr_t faulting_address;
ASSERT_OK(
zx::vmar::root_self()->allocate(0, 0, zx_system_get_page_size(), &vmar, &faulting_address));
auto cleanup_vmar = fit::defer([&vmar]() { vmar.destroy(); });
const bool have_insns = HaveInsns();
const uint32_t insn_except = have_insns ? 0 : kNoInsnsException;
TestData test = {
.write_insn = kWriteInsnValue,
.write_prop = faulting_address,
};
ASSERT_NO_FATAL_FAILURE(TestInThread(reg, test, insn_except, insn_except, faulting_address));
EXPECT_OK(test.get_status);
EXPECT_OK(test.set_status);
if (have_insns) {
EXPECT_EQ(kInitialValue, test.read_insn);
EXPECT_EQ(kWriteInsnValue, test.read_prop);
} else {
EXPECT_EQ(kUndefinedInsnValue, test.read_insn);
EXPECT_EQ(kInitialValue, test.read_prop);
}
}
TEST(Property, RegisterFsFault) { ASSERT_NO_FATAL_FAILURE(TestFault(kFs)); }
TEST(Property, RegisterGsFault) { ASSERT_NO_FATAL_FAILURE(TestFault(kGs)); }
// Both machine instructions and system calls refuse noncanonical values.
void TestNoncanonical(const WhichRegister& reg) {
const bool have_insns = HaveInsns();
const uint32_t read_exception = have_insns ? 0 : kNoInsnsException;
const uint32_t write_exception = have_insns ? kNoncanonicalException : kNoInsnsException;
TestData test = {
.write_insn = kNoncanonicalValue,
.write_prop = kNoncanonicalValue,
};
ASSERT_NO_FATAL_FAILURE(TestInThread(reg, test, read_exception, write_exception, kInitialValue));
if (have_insns) {
EXPECT_EQ(kInitialValue, test.read_insn);
} else {
EXPECT_EQ(kUndefinedInsnValue, test.read_insn);
}
// Since writing wasn't allowed, reading should still find the initial value.
EXPECT_OK(test.get_status);
EXPECT_EQ(kInitialValue, test.read_prop);
// Writing via system call should fail just like the instruction faults.
EXPECT_EQ(ZX_ERR_INVALID_ARGS, test.set_status);
}
TEST(Property, RegisterFsNoncanonical) { ASSERT_NO_FATAL_FAILURE(TestNoncanonical(kFs)); }
TEST(Property, RegisterGsNoncanonical) { ASSERT_NO_FATAL_FAILURE(TestNoncanonical(kGs)); }
// Non-user addresses are allowed even though they'll always fault when used.
void TestNonuser(const WhichRegister& reg) {
const bool have_insns = HaveInsns();
const uint32_t read_exception = have_insns ? 0 : kNoInsnsException;
const uint32_t write_exception = have_insns ? 0 : kNoInsnsException;
TestData test = {
.write_insn = kNonuserValue,
.write_prop = kNonuserValue,
};
ASSERT_NO_FATAL_FAILURE(TestInThread(reg, test, read_exception, write_exception, kNonuserValue));
EXPECT_OK(test.set_status);
EXPECT_OK(test.get_status);
if (have_insns) {
EXPECT_EQ(kInitialValue, test.read_insn);
EXPECT_EQ(kNonuserValue, test.read_prop);
} else {
EXPECT_EQ(kUndefinedInsnValue, test.read_insn);
EXPECT_EQ(kInitialValue, test.read_prop);
}
}
TEST(Property, RegisterFsNonuser) { ASSERT_NO_FATAL_FAILURE(TestNonuser(kFs)); }
TEST(Property, RegisterGsNonuser) { ASSERT_NO_FATAL_FAILURE(TestNonuser(kGs)); }
// The invalid uses of the system calls are easy to test without the
// separate test thread, since no register values will actually change.
// The system calls only work with a thread handle.
void TestNonThread(const WhichRegister& reg) {
uint64_t x = 0xfeedfacedeadbeef;
EXPECT_EQ(ZX_ERR_WRONG_TYPE, zx::process::self()->get_property(reg.property, &x, sizeof(x)));
EXPECT_EQ(ZX_ERR_WRONG_TYPE, zx::process::self()->set_property(reg.property, &x, sizeof(x)));
}
TEST(Property, RegisterFsNonThread) { ASSERT_NO_FATAL_FAILURE(TestNonThread(kFs)); }
TEST(Property, RegisterGsNonThread) { ASSERT_NO_FATAL_FAILURE(TestNonThread(kGs)); }
// The system calls only work with a handle to the calling thread.
void TestOtherThread(const WhichRegister& reg) {
// Start another thread that will block on the lock until we release it.
std::mutex lock;
std::unique_lock main_locked(lock);
std::thread other([&lock]() { std::lock_guard other_locked(lock); });
auto cleanup_thread = fit::defer([main_locked = std::move(main_locked), &other]() mutable {
main_locked.unlock();
other.join();
});
// That thread is alive, so its handle is valid.
zx::unowned_thread thread_handle{native_thread_get_zx_handle(other.native_handle())};
uint64_t x = 0xfeedfacedeadbeef;
EXPECT_EQ(ZX_ERR_ACCESS_DENIED, thread_handle->get_property(reg.property, &x, sizeof(x)));
EXPECT_EQ(ZX_ERR_ACCESS_DENIED, thread_handle->set_property(reg.property, &x, sizeof(x)));
}
TEST(Property, RegisterFsOtherThread) { ASSERT_NO_FATAL_FAILURE(TestOtherThread(kFs)); }
TEST(Property, RegisterGsOtherThread) { ASSERT_NO_FATAL_FAILURE(TestOtherThread(kGs)); }
void TestTooSmall(const WhichRegister& reg) {
uint32_t x = 0xdeadbeef;
EXPECT_EQ(ZX_ERR_BUFFER_TOO_SMALL, zx::thread::self()->get_property(reg.property, &x, sizeof(x)));
EXPECT_EQ(ZX_ERR_BUFFER_TOO_SMALL, zx::thread::self()->set_property(reg.property, &x, sizeof(x)));
}
TEST(Property, RegisterFsTooSmall) { ASSERT_NO_FATAL_FAILURE(TestTooSmall(kFs)); }
TEST(Property, RegisterGsTooSmall) { ASSERT_NO_FATAL_FAILURE(TestTooSmall(kGs)); }
} // namespace
#endif // __x86_64__