CCTLib
Calling-context and data-object attribution library for Intel Pin
splay_deserialize_test.cpp
Go to the documentation of this file.
1 // Standalone unit test for the splay-tree insertion pattern used by
2 // cctlib.cpp's CCT deserialization path (DeserializeCCTNode).
3 //
4 // The deserialize logic in question:
5 //
6 // newNode = new Splay{key, value};
7 // if (root == NULL) {
8 // root = newNode;
9 // newNode->left = NULL;
10 // newNode->right = NULL;
11 // } else {
12 // found = splay(root, key);
13 // // BUG: baseline is missing `root = newNode;` here.
14 // if (key < found->key) {
15 // newNode->left = found->left;
16 // newNode->right = found;
17 // found->left = NULL;
18 // } else {
19 // newNode->left = found;
20 // newNode->right = found->right;
21 // found->right = NULL;
22 // }
23 // }
24 //
25 // The reference implementation for live-instrumentation insertion (same file,
26 // InstrumentTraceEntry) always sets `root = newNode;` before the branch. This
27 // unit test proves the deserialize code is INCORRECT without that line by
28 // running both variants against the same sequence of inserts and comparing
29 // tree well-formedness:
30 // - well-formed: every inserted key is discoverable via `splay(root, key)`.
31 // - malformed: at least one inserted key is unreachable from `root`, i.e.
32 // the outer pointer no longer references the tree we grew.
33 //
34 // Compile-time flag SPLAY_FIX selects the fixed variant. The main() below
35 // runs BOTH and asserts the expected diagnosis (buggy fails, fixed passes).
36 
37 #include "splay-macros.h"
38 
39 #include <cstdio>
40 #include <cstdint>
41 #include <cstdlib>
42 #include <cstring>
43 #include <set>
44 #include <vector>
45 
46 struct TraceSplay {
47  uintptr_t key;
48  int value; // scalar so we can spot the leak
51 };
52 
53 static TraceSplay* splay(TraceSplay* root, uintptr_t key) {
54  REGULAR_SPLAY_TREE(TraceSplay, root, key, key, left, right);
55  return root;
56 }
57 
58 // Insert `key` into the splay tree rooted at *rootp. `apply_fix` controls
59 // whether we install newNode as the new root before the branch split.
60 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) -- when apply_fix
61 // is false the buggy path intentionally leaks newNode; that IS the bug
62 // this test demonstrates and cleans up on program exit.
63 static void insert(TraceSplay** rootp, uintptr_t key, int value, bool apply_fix) {
64  TraceSplay* newNode = new TraceSplay{key, value, nullptr, nullptr};
65  if (*rootp == nullptr) {
66  *rootp = newNode;
67  return;
68  }
69  TraceSplay* found = splay(*rootp, key);
70  // *rootp = found; // the splay left `found` as the root of the reordered tree
71 
72  if (apply_fix) {
73  *rootp = newNode;
74  }
75  if (key < found->key) {
76  newNode->left = found->left;
77  newNode->right = found;
78  found->left = nullptr;
79  } else {
80  newNode->left = found;
81  newNode->right = found->right;
82  found->right = nullptr;
83  }
84 }
85 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
86 
87 // Collect all keys reachable from `root` via left/right pointers.
88 static void collect_keys(TraceSplay* root, std::set<uintptr_t>& out) {
89  if (!root)
90  return;
91  if (!out.insert(root->key).second)
92  return; // cycle safety
93  collect_keys(root->left, out);
94  collect_keys(root->right, out);
95 }
96 
97 // Try to find `key` by repeatedly splaying and checking the root's key.
98 static bool splay_finds(TraceSplay** rootp, uintptr_t key) {
99  if (!*rootp)
100  return false;
101  *rootp = splay(*rootp, key);
102  return (*rootp)->key == key;
103 }
104 
105 static bool run_scenario(bool apply_fix, const char* label,
106  const std::vector<uintptr_t>& insertion_order) {
107  TraceSplay* root = nullptr;
108  for (size_t i = 0; i < insertion_order.size(); ++i) {
109  insert(&root, insertion_order[i], (int)i, apply_fix);
110  }
111 
112  // Collect reachable keys and verify each inserted key is discoverable
113  // via splay(root, key). Splay is destructive but idempotent for existence.
114  std::set<uintptr_t> reachable;
115  collect_keys(root, reachable);
116 
117  size_t missing_reachable = 0;
118  for (uintptr_t k : insertion_order) {
119  if (!reachable.count(k))
120  ++missing_reachable;
121  }
122 
123  // Independently, try splay-based lookup (this is how any real caller
124  // discovers whether a key is in the tree). Even if all nodes are
125  // reachable structurally, splay may fail to land on the intended root
126  // when the outer pointer went stale.
127  size_t missing_splay = 0;
128  for (uintptr_t k : insertion_order) {
129  if (!splay_finds(&root, k))
130  ++missing_splay;
131  }
132 
133  fprintf(stderr,
134  "[%s] insert=%zu reachable=%zu splay_lookups_failed=%zu missing_reachable=%zu\n",
135  label, insertion_order.size(), reachable.size(),
136  missing_splay, missing_reachable);
137  return missing_reachable == 0 && missing_splay == 0;
138 }
139 
140 // NOLINTBEGIN(bugprone-exception-escape) -- unit-test main; std::bad_alloc
141 // from std::vector allocations escaping here properly terminates the run.
142 int main() {
143  // Sequences chosen so the splay+split path fires often. First insert is
144  // always the "root=null" case; each subsequent insert exercises the
145  // buggy `else` branch.
146  std::vector<uintptr_t> asc = {100, 200, 300, 400, 500, 600, 700, 800};
147  std::vector<uintptr_t> desc = {800, 700, 600, 500, 400, 300, 200, 100};
148  std::vector<uintptr_t> mix = {500, 100, 900, 300, 700, 200, 800, 400, 600};
149 
150  struct Case {
151  const char* name;
152  const std::vector<uintptr_t>* seq;
153  };
154  Case cases[] = {
155  {"ascending", &asc},
156  {"descending", &desc},
157  {"mixed", &mix},
158  };
159 
160  int failures = 0;
161  for (const Case& c : cases) {
162  fprintf(stderr, "\n== scenario: %s ==\n", c.name);
163  (void)run_scenario(/*apply_fix=*/false, "BUGGY", *c.seq);
164  bool fixed_ok = run_scenario(/*apply_fix=*/true, "FIXED", *c.seq);
165 
166  // The test's diagnostic contract:
167  // FIXED must always pass. If it doesn't, the fix is wrong.
168  // BUGGY must FAIL on at least one of the scenarios. If it doesn't,
169  // the code path we thought was broken is actually fine and the
170  // splay-tree fix is unnecessary.
171  if (!fixed_ok) {
172  fprintf(stderr, "[FAIL] FIXED scenario '%s' should have passed\n", c.name);
173  ++failures;
174  }
175  }
176 
177  // Composite bug demonstration: BUGGY must fail on at least one scenario.
178  fprintf(stderr, "\n== composite (BUGGY must fail at least one scenario) ==\n");
179  bool any_buggy_failure = false;
180  for (const Case& c : cases) {
181  if (!run_scenario(/*apply_fix=*/false, c.name, *c.seq)) {
182  any_buggy_failure = true;
183  }
184  }
185  if (!any_buggy_failure) {
186  fprintf(stderr, "[FAIL] baseline BUGGY variant unexpectedly passed all scenarios\n");
187  ++failures;
188  } else {
189  fprintf(stderr, "[OK ] baseline BUGGY variant does fail as expected\n");
190  }
191 
192  fprintf(stderr, "\n%d failures\n", failures);
193  return failures == 0 ? 0 : 1;
194 }
195 // NOLINTEND(bugprone-exception-escape)
uint8_t value[MAX_WRITE_OP_LENGTH]
#define REGULAR_SPLAY_TREE(type, root, key, value, left, right)
Definition: splay-macros.h:124
static void insert(TraceSplay **rootp, uintptr_t key, int value, bool apply_fix)
static bool run_scenario(bool apply_fix, const char *label, const std::vector< uintptr_t > &insertion_order)
static bool splay_finds(TraceSplay **rootp, uintptr_t key)
static void collect_keys(TraceSplay *root, std::set< uintptr_t > &out)
static TraceSplay * splay(TraceSplay *root, uintptr_t key)
int mix(int a, int b)
Definition: test3.c:7