| /* Copyright (c) 2019, Google Inc. | |
| * | |
| * Permission to use, copy, modify, and/or distribute this software for any | |
| * purpose with or without fee is hereby granted, provided that the above | |
| * copyright notice and this permission notice appear in all copies. | |
| * | |
| * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
| * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
| * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY | |
| * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
| * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION | |
| * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | |
| * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ | |
| #include <gtest/gtest.h> | |
| #include <stdlib.h> | |
| #include <openssl/rand.h> | |
| #include "internal.h" | |
| #include "getrandom_fillin.h" | |
| #if defined(OPENSSL_X86_64) && !defined(BORINGSSL_SHARED_LIBRARY) && \ | |
| !defined(BORINGSSL_UNSAFE_DETERMINISTIC_MODE) && defined(USE_NR_getrandom) | |
| #include <linux/random.h> | |
| #include <sys/ptrace.h> | |
| #include <sys/syscall.h> | |
| #include <sys/user.h> | |
| #include "fork_detect.h" | |
| #if !defined(PTRACE_O_EXITKILL) | |
| #define PTRACE_O_EXITKILL (1 << 20) | |
| #endif | |
| // This test can be run with $OPENSSL_ia32cap=~0x4000000000000000 in order to | |
| // simulate the absence of RDRAND of machines that have it. | |
| // Event represents a system call from urandom.c that is observed by the ptrace | |
| // code in |GetTrace|. | |
| struct Event { | |
| enum class Syscall { | |
| kGetRandom, | |
| kOpen, | |
| kUrandomRead, | |
| kUrandomIoctl, | |
| kAbort, | |
| }; | |
| explicit Event(Syscall syscall) : type(syscall) {} | |
| bool operator==(const Event &other) const { | |
| return type == other.type && length == other.length && | |
| flags == other.flags && | |
| ((filename == nullptr && other.filename == nullptr) || | |
| strcmp(filename, other.filename) == 0); | |
| } | |
| static Event GetRandom(size_t length, unsigned flags) { | |
| Event e(Syscall::kGetRandom); | |
| e.length = length; | |
| e.flags = flags; | |
| return e; | |
| } | |
| static Event Open(const char *filename) { | |
| Event e(Syscall::kOpen); | |
| e.filename = filename; | |
| return e; | |
| } | |
| static Event UrandomRead(size_t length) { | |
| Event e(Syscall::kUrandomRead); | |
| e.length = length; | |
| return e; | |
| } | |
| static Event UrandomIoctl() { | |
| Event e(Syscall::kUrandomIoctl); | |
| return e; | |
| } | |
| static Event Abort() { | |
| Event e(Syscall::kAbort); | |
| return e; | |
| } | |
| std::string String() const { | |
| char buf[256]; | |
| switch (type) { | |
| case Syscall::kGetRandom: | |
| snprintf(buf, sizeof(buf), "getrandom(_, %zu, %d)", length, flags); | |
| break; | |
| case Syscall::kOpen: | |
| snprintf(buf, sizeof(buf), "open(%s, _)", filename); | |
| break; | |
| case Syscall::kUrandomRead: | |
| snprintf(buf, sizeof(buf), "read(urandom_fd, _, %zu)", length); | |
| break; | |
| case Syscall::kUrandomIoctl: | |
| return "ioctl(urandom_fd, RNDGETENTCNT, _)"; | |
| case Syscall::kAbort: | |
| return "abort()"; | |
| } | |
| return std::string(buf); | |
| } | |
| const Syscall type; | |
| size_t length = 0; | |
| unsigned flags = 0; | |
| const char *filename = nullptr; | |
| }; | |
| static std::string ToString(const std::vector<Event> &trace) { | |
| std::string ret; | |
| for (const auto &event : trace) { | |
| if (!ret.empty()) { | |
| ret += ", "; | |
| } | |
| ret += event.String(); | |
| } | |
| return ret; | |
| } | |
| // The following are flags to tell |GetTrace| to inject faults, using ptrace, | |
| // into the entropy-related system calls. | |
| // getrandom gives |ENOSYS|. | |
| static const unsigned NO_GETRANDOM = 1; | |
| // opening /dev/urandom fails. | |
| static const unsigned NO_URANDOM = 2; | |
| // getrandom always returns |EAGAIN| if given |GRNG_NONBLOCK|. | |
| static const unsigned GETRANDOM_NOT_READY = 4; | |
| // The ioctl on urandom returns only 255 bits of entropy the first time that | |
| // it's called. | |
| static const unsigned URANDOM_NOT_READY = 8; | |
| // getrandom gives |EINVAL| unless |NO_GETRANDOM| is set. | |
| static const unsigned GETRANDOM_ERROR = 16; | |
| // Reading from /dev/urandom gives |EINVAL|. | |
| static const unsigned URANDOM_ERROR = 32; | |
| static const unsigned NEXT_FLAG = 64; | |
| // GetTrace runs |thunk| in a forked process and observes the resulting system | |
| // calls using ptrace. It simulates a variety of failures based on the contents | |
| // of |flags| and records the observed events by appending to |out_trace|. | |
| static void GetTrace(std::vector<Event> *out_trace, unsigned flags, | |
| std::function<void()> thunk) { | |
| const int child_pid = fork(); | |
| ASSERT_NE(-1, child_pid); | |
| if (child_pid == 0) { | |
| // Child process | |
| if (ptrace(PTRACE_TRACEME, 0, 0, 0) != 0) { | |
| perror("PTRACE_TRACEME"); | |
| _exit(1); | |
| } | |
| raise(SIGSTOP); | |
| thunk(); | |
| _exit(0); | |
| } | |
| // Parent process | |
| int status; | |
| ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); | |
| ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP); | |
| // Set options so that: | |
| // a) the child process is killed once this process dies. | |
| // b) System calls result in a WSTOPSIG value of (SIGTRAP | 0x80) rather | |
| // than just SIGTRAP. (This doesn't matter here, but it's recommended | |
| // practice so that it's distinct from the signal itself.) | |
| ASSERT_EQ(0, ptrace(PTRACE_SETOPTIONS, child_pid, nullptr, | |
| PTRACE_O_EXITKILL | PTRACE_O_TRACESYSGOOD)) | |
| << strerror(errno); | |
| // urandom_fd tracks the file descriptor number for /dev/urandom in the child | |
| // process, if it opens it. | |
| int urandom_fd = -1; | |
| for (;;) { | |
| // Advance the child to the next system call. | |
| ASSERT_EQ(0, ptrace(PTRACE_SYSCALL, child_pid, 0, 0)); | |
| ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); | |
| // The child may have aborted rather than made a system call. | |
| if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGABRT) { | |
| out_trace->push_back(Event::Abort()); | |
| break; | |
| } | |
| // Otherwise the only valid ptrace event is a system call stop. | |
| ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)); | |
| struct user_regs_struct regs; | |
| ASSERT_EQ(0, ptrace(PTRACE_GETREGS, child_pid, nullptr, ®s)); | |
| const auto syscall_number = regs.orig_rax; | |
| bool is_opening_urandom = false; | |
| bool is_urandom_ioctl = false; | |
| uintptr_t ioctl_output_addr = 0; | |
| // inject_error is zero to indicate that the system call should run | |
| // normally. Otherwise it's, e.g. -EINVAL, to indicate that the system call | |
| // should not run and that error should be injected on return. | |
| int inject_error = 0; | |
| switch (syscall_number) { | |
| case __NR_getrandom: | |
| if (flags & NO_GETRANDOM) { | |
| inject_error = -ENOSYS; | |
| } else if (flags & GETRANDOM_ERROR) { | |
| inject_error = -EINVAL; | |
| } else if (flags & GETRANDOM_NOT_READY) { | |
| if (regs.rdx & GRND_NONBLOCK) { | |
| inject_error = -EAGAIN; | |
| } | |
| } | |
| out_trace->push_back( | |
| Event::GetRandom(/*length=*/regs.rsi, /*flags=*/regs.rdx)); | |
| break; | |
| case __NR_openat: | |
| case __NR_open: { | |
| // It's assumed that any arguments to open(2) are constants in read-only | |
| // memory and thus the pointer in the child's context will also be a | |
| // valid pointer in our address space. | |
| const char *filename = reinterpret_cast<const char *>( | |
| (syscall_number == __NR_openat) ? regs.rsi : regs.rdi); | |
| out_trace->push_back(Event::Open(filename)); | |
| is_opening_urandom = strcmp(filename, "/dev/urandom") == 0; | |
| if (is_opening_urandom && (flags & NO_URANDOM)) { | |
| inject_error = -ENOENT; | |
| } | |
| break; | |
| } | |
| case __NR_read: { | |
| const int read_fd = regs.rdi; | |
| if (urandom_fd >= 0 && urandom_fd == read_fd) { | |
| out_trace->push_back(Event::UrandomRead(/*length=*/regs.rdx)); | |
| if (flags & URANDOM_ERROR) { | |
| inject_error = -EINVAL; | |
| } | |
| } | |
| break; | |
| } | |
| case __NR_ioctl: { | |
| const int ioctl_fd = regs.rdi; | |
| if (urandom_fd >= 0 && ioctl_fd == urandom_fd && | |
| regs.rsi == RNDGETENTCNT) { | |
| out_trace->push_back(Event::UrandomIoctl()); | |
| is_urandom_ioctl = true; | |
| ioctl_output_addr = regs.rdx; | |
| } | |
| } | |
| } | |
| if (inject_error) { | |
| // Replace the system call number with -1 to cause the kernel to ignore | |
| // the call. The -ENOSYS will be replaced later with the value of | |
| // |inject_error|. | |
| regs.orig_rax = -1; | |
| ASSERT_EQ(0, ptrace(PTRACE_SETREGS, child_pid, nullptr, ®s)); | |
| } | |
| ASSERT_EQ(0, ptrace(PTRACE_SYSCALL, child_pid, 0, 0)); | |
| ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); | |
| // If the system call was exit/exit_group, the process may be terminated | |
| // rather than have exited the system call. | |
| if (WIFEXITED(status)) { | |
| ASSERT_EQ(0, WEXITSTATUS(status)); | |
| return; | |
| } | |
| // Otherwise the next state must be a system call exit stop. This is | |
| // indistinguishable from a system call entry, we just have to keep track | |
| // and know that these events happen in pairs. | |
| ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)); | |
| if (inject_error) { | |
| if (inject_error != -ENOSYS) { | |
| ASSERT_EQ(0, ptrace(PTRACE_GETREGS, child_pid, nullptr, ®s)); | |
| regs.rax = inject_error; | |
| ASSERT_EQ(0, ptrace(PTRACE_SETREGS, child_pid, nullptr, ®s)); | |
| } | |
| } else if (is_opening_urandom) { | |
| ASSERT_EQ(0, ptrace(PTRACE_GETREGS, child_pid, nullptr, ®s)); | |
| urandom_fd = regs.rax; | |
| } else if (is_urandom_ioctl) { | |
| // The result is the number of bits of entropy that the kernel currently | |
| // believes that it has. urandom.c waits until 256 bits are ready. | |
| int result = 256; | |
| // If we are simulating urandom not being ready then we have the ioctl | |
| // indicate one too few bits of entropy the first time it's queried. | |
| if (flags & URANDOM_NOT_READY) { | |
| result--; | |
| flags &= ~URANDOM_NOT_READY; | |
| } | |
| // ptrace always works with ill-defined "words", which appear to be 64-bit | |
| // on x86-64. Since the ioctl result is a 32-bit int, do a | |
| // read-modify-write to inject the answer. | |
| const uintptr_t aligned_addr = ioctl_output_addr & ~7; | |
| const uintptr_t offset = ioctl_output_addr - aligned_addr; | |
| union { | |
| uint64_t word; | |
| uint8_t bytes[8]; | |
| } u; | |
| u.word = ptrace(PTRACE_PEEKDATA, child_pid, | |
| reinterpret_cast<void *>(aligned_addr), nullptr); | |
| memcpy(&u.bytes[offset], &result, sizeof(result)); | |
| ASSERT_EQ(0, ptrace(PTRACE_POKEDATA, child_pid, | |
| reinterpret_cast<void *>(aligned_addr), | |
| reinterpret_cast<void *>(u.word))); | |
| } | |
| } | |
| } | |
| // TestFunction is the function that |GetTrace| is asked to trace. | |
| static void TestFunction() { | |
| uint8_t byte; | |
| RAND_bytes(&byte, sizeof(byte)); | |
| RAND_bytes(&byte, sizeof(byte)); | |
| } | |
| static bool have_fork_detection() { | |
| return CRYPTO_get_fork_generation() != 0; | |
| } | |
| // TestFunctionPRNGModel is a model of how the urandom.c code will behave when | |
| // |TestFunction| is run. It should return the same trace of events that | |
| // |GetTrace| will observe the real code making. | |
| static std::vector<Event> TestFunctionPRNGModel(unsigned flags) { | |
| #if defined(BORINGSSL_FIPS) | |
| static const bool is_fips = true; | |
| #else | |
| static const bool is_fips = false; | |
| #endif | |
| std::vector<Event> ret; | |
| bool urandom_probed = false; | |
| bool getrandom_ready = false; | |
| // Probe for getrandom support | |
| ret.push_back(Event::GetRandom(1, GRND_NONBLOCK)); | |
| std::function<void()> wait_for_entropy; | |
| std::function<bool(bool, size_t)> sysrand; | |
| if (flags & NO_GETRANDOM) { | |
| ret.push_back(Event::Open("/dev/urandom")); | |
| if (flags & NO_URANDOM) { | |
| ret.push_back(Event::Abort()); | |
| return ret; | |
| } | |
| wait_for_entropy = [&ret, &urandom_probed, flags] { | |
| if (!is_fips || urandom_probed) { | |
| return; | |
| } | |
| // Probe urandom for entropy. | |
| ret.push_back(Event::UrandomIoctl()); | |
| if (flags & URANDOM_NOT_READY) { | |
| // If the first attempt doesn't report enough entropy, probe | |
| // repeatedly until it does, which will happen with the second attempt. | |
| ret.push_back(Event::UrandomIoctl()); | |
| } | |
| urandom_probed = true; | |
| }; | |
| sysrand = [&ret, &wait_for_entropy, flags](bool block, size_t len) { | |
| if (block) { | |
| wait_for_entropy(); | |
| } | |
| ret.push_back(Event::UrandomRead(len)); | |
| if (flags & URANDOM_ERROR) { | |
| ret.push_back(Event::Abort()); | |
| return false; | |
| } | |
| return true; | |
| }; | |
| } else { | |
| if (flags & GETRANDOM_ERROR) { | |
| ret.push_back(Event::Abort()); | |
| return ret; | |
| } | |
| getrandom_ready = (flags & GETRANDOM_NOT_READY) == 0; | |
| wait_for_entropy = [&ret, &getrandom_ready] { | |
| if (getrandom_ready) { | |
| return; | |
| } | |
| ret.push_back(Event::GetRandom(1, GRND_NONBLOCK)); | |
| ret.push_back(Event::GetRandom(1, 0)); | |
| getrandom_ready = true; | |
| }; | |
| sysrand = [&ret, &wait_for_entropy](bool block, size_t len) { | |
| if (block) { | |
| wait_for_entropy(); | |
| } | |
| ret.push_back(Event::GetRandom(len, block ? 0 : GRND_NONBLOCK)); | |
| return true; | |
| }; | |
| } | |
| const size_t kSeedLength = CTR_DRBG_ENTROPY_LEN * (is_fips ? 10 : 1); | |
| const size_t kAdditionalDataLength = 32; | |
| if (!have_rdrand()) { | |
| if ((!have_fork_detection() && !sysrand(true, kAdditionalDataLength)) || | |
| // Initialise CRNGT. | |
| (is_fips && !sysrand(true, 16)) || | |
| !sysrand(true, kSeedLength) || | |
| // Second entropy draw. | |
| (!have_fork_detection() && !sysrand(true, kAdditionalDataLength))) { | |
| return ret; | |
| } | |
| } else if ( | |
| // First additional data. If fast RDRAND isn't available then a | |
| // non-blocking OS entropy draw will be tried. | |
| (!have_fast_rdrand() && !have_fork_detection() && | |
| !sysrand(false, kAdditionalDataLength)) || | |
| // Opportuntistic entropy draw in FIPS mode because RDRAND was used. | |
| // In non-FIPS mode it's just drawn from |CRYPTO_sysrand| in a blocking | |
| // way. | |
| !sysrand(!is_fips, CTR_DRBG_ENTROPY_LEN) || | |
| // Second entropy draw's additional data. | |
| (!have_fast_rdrand() && !have_fork_detection() && | |
| !sysrand(false, kAdditionalDataLength))) { | |
| return ret; | |
| } | |
| return ret; | |
| } | |
| static void CheckInvariants(const std::vector<Event> &events) { | |
| // If RDRAND is available then there should be no blocking syscalls in FIPS | |
| // mode. | |
| #if defined(BORINGSSL_FIPS) | |
| if (have_rdrand()) { | |
| for (const auto &event : events) { | |
| switch (event.type) { | |
| case Event::Syscall::kGetRandom: | |
| if ((event.flags & GRND_NONBLOCK) == 0) { | |
| ADD_FAILURE() << "Blocking getrandom found with RDRAND: " | |
| << ToString(events); | |
| } | |
| break; | |
| case Event::Syscall::kUrandomIoctl: | |
| ADD_FAILURE() << "Urandom polling found with RDRAND: " | |
| << ToString(events); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| } | |
| #endif | |
| } | |
| // Tests that |TestFunctionPRNGModel| is a correct model for the code in | |
| // urandom.c, at least to the limits of the the |Event| type. | |
| TEST(URandomTest, Test) { | |
| char buf[256]; | |
| #define TRACE_FLAG(flag) \ | |
| snprintf(buf, sizeof(buf), #flag ": %d", (flags & flag) != 0); \ | |
| SCOPED_TRACE(buf); | |
| for (unsigned flags = 0; flags < NEXT_FLAG; flags++) { | |
| TRACE_FLAG(NO_GETRANDOM); | |
| TRACE_FLAG(NO_URANDOM); | |
| TRACE_FLAG(GETRANDOM_NOT_READY); | |
| TRACE_FLAG(URANDOM_NOT_READY); | |
| TRACE_FLAG(GETRANDOM_ERROR); | |
| TRACE_FLAG(URANDOM_ERROR); | |
| const std::vector<Event> expected_trace = TestFunctionPRNGModel(flags); | |
| CheckInvariants(expected_trace); | |
| std::vector<Event> actual_trace; | |
| GetTrace(&actual_trace, flags, TestFunction); | |
| if (expected_trace != actual_trace) { | |
| ADD_FAILURE() << "Expected: " << ToString(expected_trace) | |
| << "\nFound: " << ToString(actual_trace); | |
| } | |
| } | |
| } | |
| int main(int argc, char **argv) { | |
| ::testing::InitGoogleTest(&argc, argv); | |
| if (getenv("BORINGSSL_IGNORE_MADV_WIPEONFORK")) { | |
| CRYPTO_fork_detect_ignore_madv_wipeonfork_for_testing(); | |
| } | |
| return RUN_ALL_TESTS(); | |
| } | |
| #else | |
| int main(int argc, char **argv) { | |
| printf("PASS\n"); | |
| return 0; | |
| } | |
| #endif // X86_64 && !SHARED_LIBRARY && !UNSAFE_DETERMINISTIC_MODE && | |
| // USE_NR_getrandom |