CCTLib
Calling-context and data-object attribution library for Intel Pin
shadow_memory_unittest.cpp
Go to the documentation of this file.
1 // Standalone unit tests for src/shadow_memory.H. Not a Pin tool -- built and
2 // run directly by `make check`.
3 //
4 // The header is a two-level page table that hands out shadow pages of
5 // SHADOW_PAGE_SIZE (65536) entries per type in a parameter pack. Two variants
6 // exist: ShadowMemory (single-threaded, raw pointers) and ConcurrentShadowMemory
7 // (atomics + CAS). Both expose GetOrCreateShadowBaseAddress(addr) returning a
8 // reference to the tuple that owns the shadow page.
9 //
10 // A free function template GetOrCreateShadowAddress<I>(sm, addr) sits on top
11 // and hands back a typed pointer to the single shadow slot for `addr`. This
12 // unittest primarily targets that free function, because its historical form
13 // used `auto shadowPage = sm.GetOrCreateShadowBaseAddress(address);` -- `auto`
14 // on a reference-returning call strips the reference and value-copies the tuple
15 // (a T[65536] per pack element) into the caller's stack, then returns a pointer
16 // into the copy. Writes through that pointer never reach real shadow memory,
17 // and subsequent reads observe zero-initialized copies.
18 //
19 // Each test writes through the free function and reads back both via the free
20 // function and via the ground-truth path (GetOrCreateShadowBaseAddress + get<>)
21 // so a divergence between the two immediately fingerprints the bug.
22 
23 #include <cstdio>
24 #include <cstdint>
25 #include <cstdlib>
26 #include <cstring>
27 #include <thread>
28 #include <vector>
29 #include <atomic>
30 
31 // The Pin build path brings <stdio.h> in transitively before shadow_memory.H;
32 // when compiling standalone we need `perror` visible for the header's ADL-free
33 // calls, so include stdio (already done above) before pulling the header in.
34 #include "shadow_memory.H"
35 
36 static int g_failures = 0;
37 static int g_checks = 0;
38 
39 #define CHECK_EQ(actual, expected, msg) \
40  do { \
41  ++g_checks; \
42  auto _a = (actual); \
43  auto _e = (expected); \
44  if (_a != _e) { \
45  ++g_failures; \
46  fprintf(stderr, " FAIL %s:%d %s: got %lld expected %lld\n", \
47  __FILE__, __LINE__, msg, \
48  (long long)_a, (long long)_e); \
49  } \
50  } while (0)
51 
52 #define RUN_TEST(fn) \
53  do { \
54  int before = g_failures; \
55  fprintf(stderr, "[RUN ] %s\n", #fn); \
56  fn(); \
57  fprintf(stderr, "[%s] %s\n", \
58  (g_failures == before) ? "PASS" : "FAIL", #fn); \
59  } while (0)
60 
61 
62 // ---------------------------------------------------------------------------
63 // Tests for ShadowMemory (single-threaded)
64 // ---------------------------------------------------------------------------
65 
66 // A single write followed by a read of the SAME address must return the
67 // written value. This is the minimum contract of the API and the smoking gun
68 // for the auto-copy bug.
71  const size_t addr = 0x1234'5678'9abc'0000ULL;
72  const uint64_t value = 0xdeadbeefcafef00dULL;
73 
74  *GetOrCreateShadowAddress<0>(sm, addr) = value;
75  uint64_t readback = *GetOrCreateShadowAddress<0>(sm, addr);
76  CHECK_EQ(readback, value, "single write/read");
77 }
78 
79 // Distinct addresses inside the SAME shadow page must not collide, and reads
80 // must see the last write to each address.
83  const size_t base = 0x1000'0000ULL;
84  for (int i = 0; i < 4096; ++i) {
85  *GetOrCreateShadowAddress<0>(sm, base + i * 8) = uint64_t{static_cast<uint64_t>(i)} * 7 + 3;
86  }
87  for (int i = 0; i < 4096; ++i) {
88  uint64_t v = *GetOrCreateShadowAddress<0>(sm, base + i * 8);
89  CHECK_EQ(v, uint64_t{static_cast<uint64_t>(i)} * 7 + 3, "same-page slot");
90  }
91 }
92 
93 // Distinct pages (l1/l2 slots differ) must not alias. This exercises page
94 // creation on both levels.
97  const size_t addrs[] = {
98  0x0000'0000ULL,
99  0x0001'0000ULL, // next l2 slot
100  0x0000'1000'0000ULL, // next l1 slot
101  0x1234'5678'0000ULL, // arbitrary
102  0x7fff'ffff'0000ULL, // near top of a 47-bit user space
103  };
104  const int n = sizeof(addrs) / sizeof(addrs[0]);
105  for (int i = 0; i < n; ++i) {
106  *GetOrCreateShadowAddress<0>(sm, addrs[i]) = 0xa5a5a5a5'00000000ULL | i;
107  }
108  for (int i = 0; i < n; ++i) {
109  uint64_t v = *GetOrCreateShadowAddress<0>(sm, addrs[i]);
110  CHECK_EQ(v, 0xa5a5a5a5'00000000ULL | i, "cross-page slot");
111  }
112 }
113 
114 // A tuple-of-mixed-types shadow (as used by data-centric code paths, e.g.
115 // ConcurrentShadowMemory<uint8_t, ContextHandle_t>) exercises tuple::get<I>
116 // for I != 0.
119  const size_t addr = 0xcafeb0ba'0000ULL;
120  *GetOrCreateShadowAddress<0>(sm, addr) = 0x5a;
121  *GetOrCreateShadowAddress<1>(sm, addr) = 0xdeadbeef;
122  CHECK_EQ(*GetOrCreateShadowAddress<0>(sm, addr), 0x5a, "type-0 slot");
123  CHECK_EQ(*GetOrCreateShadowAddress<1>(sm, addr), 0xdeadbeef, "type-1 slot");
124 }
125 
126 // The free function must agree with the direct GetOrCreateShadowBaseAddress
127 // path -- writing through one and reading through the other proves shadow
128 // memory is actually mutated (i.e. the write did not land in a stack copy).
131  const size_t addr = 0x0f0f'0000'1234ULL;
132  const uint64_t value = 0x1122334455667788ULL;
133 
134  *GetOrCreateShadowAddress<0>(sm, addr) = value;
135  ShadowTuple<uint64_t>& page = sm.GetOrCreateShadowBaseAddress(addr);
136  uint64_t via_base = std::get<0>(page)[PAGE_OFFSET(addr)];
137  CHECK_EQ(via_base, value, "write via free-fn observed via base-fn");
138 }
139 
140 // Symmetric of the above: write via base function, read via free function.
143  const size_t addr = 0x0e0e'ffff'5678ULL;
144  const uint64_t value = 0x8899aabbccddeeffULL;
145 
146  ShadowTuple<uint64_t>& page = sm.GetOrCreateShadowBaseAddress(addr);
147  std::get<0>(page)[PAGE_OFFSET(addr)] = value;
148  uint64_t via_free = *GetOrCreateShadowAddress<0>(sm, addr);
149  CHECK_EQ(via_free, value, "write via base-fn observed via free-fn");
150 }
151 
152 // A pointer returned from GetOrCreateShadowAddress on address A must be equal
153 // to a pointer subsequently returned for the same A -- i.e. every call must
154 // point into the same underlying shadow page, not a fresh stack copy.
157  const size_t addr = 0x2222'3333'4444ULL;
158  uint64_t* p1 = GetOrCreateShadowAddress<0>(sm, addr);
159  uint64_t* p2 = GetOrCreateShadowAddress<0>(sm, addr);
160  CHECK_EQ(p1 == p2, true, "pointer stability across calls");
161 }
162 
163 // Interleaved writes at different offsets inside one page must all persist.
164 // This is the exact pattern InitShadowSpaceForDataCentric uses (write N
165 // consecutive slots via successive GetOrCreateShadowAddress calls).
168  const size_t base = 0x8000'0000ULL;
169  for (int i = 0; i < 256; ++i) {
170  *GetOrCreateShadowAddress<0>(sm, base + i * 8) = 0xf00d0000ULL + i;
171  }
172  for (int i = 0; i < 256; ++i) {
173  CHECK_EQ(*GetOrCreateShadowAddress<0>(sm, base + i * 8),
174  0xf00d0000ULL + i, "init-pattern slot");
175  }
176 }
177 
178 
179 // ---------------------------------------------------------------------------
180 // Same battery for ConcurrentShadowMemory
181 // ---------------------------------------------------------------------------
182 
185  const size_t addr = 0x1234'5678'9abc'0000ULL;
186  const uint64_t value = 0xdeadbeefcafef00dULL;
187 
188  *GetOrCreateShadowAddress<0>(sm, addr) = value;
189  uint64_t readback = *GetOrCreateShadowAddress<0>(sm, addr);
190  CHECK_EQ(readback, value, "single write/read (concurrent)");
191 }
192 
195  const size_t addr = 0x0f0f'0000'1234ULL;
196  const uint64_t value = 0x1122334455667788ULL;
197 
198  *GetOrCreateShadowAddress<0>(sm, addr) = value;
199  ShadowTuple<uint64_t>& page = sm.GetOrCreateShadowBaseAddress(addr);
200  uint64_t via_base = std::get<0>(page)[PAGE_OFFSET(addr)];
201  CHECK_EQ(via_base, value, "concurrent: write via free-fn observed via base-fn");
202 }
203 
206  const size_t addr = 0xcafeb0ba'0000ULL;
207  *GetOrCreateShadowAddress<0>(sm, addr) = 0x5a;
208  *GetOrCreateShadowAddress<1>(sm, addr) = 0xdeadbeef;
209  CHECK_EQ(*GetOrCreateShadowAddress<0>(sm, addr), 0x5a, "type-0 slot (concurrent)");
210  CHECK_EQ(*GetOrCreateShadowAddress<1>(sm, addr), 0xdeadbeef, "type-1 slot (concurrent)");
211 }
212 
213 // N threads each write a unique value into disjoint shadow addresses through
214 // the free-function overload, then all threads read back and verify. Exercises
215 // CAS-based page creation under contention plus the write-through path.
218  const int nthreads = 8;
219  const int per_thread = 1024;
220  const size_t stride = 0x100'0000ULL; // one entry per l2 slot
221 
222  std::atomic<int> mismatches{0};
223 
224  auto writer = [&](int tid) {
225  for (int i = 0; i < per_thread; ++i) {
226  size_t addr = (size_t)tid * stride + (size_t)i * 8;
227  *GetOrCreateShadowAddress<0>(sm, addr) = (uint64_t)tid * 1000000ULL + i;
228  }
229  };
230  std::vector<std::thread> ts;
231  ts.reserve(nthreads);
232  for (int t = 0; t < nthreads; ++t)
233  ts.emplace_back(writer, t);
234  for (auto& th : ts)
235  th.join();
236 
237  auto reader = [&](int tid) {
238  for (int i = 0; i < per_thread; ++i) {
239  size_t addr = (size_t)tid * stride + (size_t)i * 8;
240  uint64_t v = *GetOrCreateShadowAddress<0>(sm, addr);
241  if (v != (uint64_t)tid * 1000000ULL + i)
242  mismatches.fetch_add(1);
243  }
244  };
245  ts.clear();
246  for (int t = 0; t < nthreads; ++t)
247  ts.emplace_back(reader, t);
248  for (auto& th : ts)
249  th.join();
250 
251  CHECK_EQ(mismatches.load(), 0, "concurrent multi-thread write/read mismatches");
252 }
253 
254 // Mimics the exact usage cctlib.cpp::InitShadowSpaceForDataCentric applies to
255 // the shadow, which is the sole write path for USE_SHADOW_FOR_DATA_CENTRIC.
256 // If the free-function overload does not persist writes, GetDataObjectHandle
257 // will silently return the zero-initialized shadow forever.
259  struct FakeHandle {
260  uint8_t type;
261  uint32_t path;
262  }; // ~ DataHandle_t
264 
265  const size_t base = 0x7ff0'0000'0000ULL;
266  const int n = 128;
267  for (int i = 0; i < n; ++i) {
268  FakeHandle* slot = GetOrCreateShadowAddress<0>(sm, base + i);
269  slot->type = 2; // DYNAMIC_OBJECT
270  slot->path = 0xabc00000u + (uint32_t)i;
271  }
272  for (int i = 0; i < n; ++i) {
273  FakeHandle h = *GetOrCreateShadowAddress<0>(sm, base + i);
274  CHECK_EQ((int)h.type, 2, "data-centric shape type persisted");
275  CHECK_EQ(h.path, 0xabc00000u + (uint32_t)i, "data-centric shape path persisted");
276  }
277 }
278 
279 
280 // NOLINTBEGIN(bugprone-exception-escape) -- unit-test main; std::bad_alloc
281 // (from thread/vector allocations) escaping here properly terminates the run.
282 int main() {
291 
297 
298  fprintf(stderr, "\n%d checks, %d failures\n", g_checks, g_failures);
299  return g_failures == 0 ? 0 : 1;
300 }
301 // NOLINTEND(bugprone-exception-escape)
static ConcurrentShadowMemory< DataHandle_t > sm
Definition: cctlib.cpp:2479
uint16_t size_t
Definition: cctlib.cpp:3359
uint8_t value[MAX_WRITE_OP_LENGTH]
std::tuple< Args[SHADOW_PAGE_SIZE]... > ShadowTuple
Definition: shadow_memory.H:56
#define PAGE_OFFSET(addr)
Definition: shadow_memory.H:15
static void test_shadow_multi_type_tuple()
static void test_shadow_write_read_single()
static void test_concurrent_free_fn_matches_base_fn()
static int g_failures
static void test_concurrent_write_read_single()
static void test_shadow_init_pattern()
static void test_concurrent_init_pattern_data_centric_shape()
#define RUN_TEST(fn)
static int g_checks
static void test_shadow_multiple_pages()
static void test_concurrent_multi_type_tuple()
static void test_shadow_multiple_addresses_same_page()
static void test_concurrent_multi_thread()
static void test_shadow_pointer_stable()
static void test_shadow_base_fn_matches_free_fn()
static void test_shadow_free_fn_matches_base_fn()
#define CHECK_EQ(actual, expected, msg)
void h()
Definition: test1.c:12