CCTLib
Calling-context and data-object attribution library for Intel Pin
cct_shape_check.cpp
Go to the documentation of this file.
1 // CCT-shape check tool for cctlib integration tests.
2 //
3 // This is NOT a general-purpose profiling client and NOT a text
4 // reporter. It runs a per-victim assertion set programmatically: at
5 // Fini, it walks the CCT (built by cctlib during the run) via
6 // GetFullCallingContext, builds an in-memory inventory of every
7 // reached call chain, and dispatches to a check function selected by
8 // the -check knob. If any assertion fails, the tool writes a short
9 // diagnostic to stderr and exits non-zero; the host-side gtest just
10 // checks the exit code and surfaces stderr on failure.
11 //
12 // Design notes:
13 // * Every recorded ContextHandle_t is turned into a stable chain
14 // signature (a vector<string> of function names, root-first, root
15 // sentinel skipped). Assertions consume the CctInventory directly.
16 // * cctlib caps GetFullCallingContext at MAX_CCT_PRINT_DEPTH=20 and
17 // appends a "Truncated call path" sentinel past that. If ANY chain
18 // shows a sentinel (truncated / CRASHED / BAD IP / FAILED_TO_READ)
19 // the tool fails the run -- those indicate cctlib silently
20 // degraded during the walk. Victim recursion depths are sized so
21 // no sentinel ever appears on the happy path.
22 // * Model on cct_metric_client.cpp (skeleton) and ins_reuse_client.cpp
23 // (per-thread TLS pattern).
24 
25 #include <cstdio>
26 #include <cstdint>
27 #include <cstring>
28 #include <cstdlib>
29 #include <algorithm>
30 #include <string>
31 #include <vector>
32 #include <map>
33 #include <set>
34 #include <unordered_map>
35 #include <sstream>
36 #include <iostream>
37 #include <unistd.h>
38 #include "pin.H"
39 #include "cctlib.H"
40 
41 using namespace std;
42 using namespace PinCCTLib;
43 
44 static KNOB<string> KnobCheck(KNOB_MODE_WRITEONCE, "pintool", "check", "",
45  "name of the per-victim CCT-shape check function to run at Fini (required)");
46 
47 // ---------------- Per-thread hit table -----------------------------
48 
49 struct TData {
50  // Every distinct ContextHandle_t reached at least once by an
51  // instrumented ins, with hit count. Bounded by number of distinct
52  // CCT positions the victim actually visits.
53  unordered_map<uint32_t, uint64_t> hits;
54  // Captured at main's entry via a RTN_InsertCall hook. Used at
55  // walk time to slice each recorded chain to the sub-chain that
56  // lives under main -- pre-main libc/loader ancestors (_start,
57  // __libc_start_main, __libc_init_first) are stripped, and any
58  // chain that never went through main is dropped entirely. This
59  // gives per-victim assertions a much tighter search space so
60  // exact-count checks (chainCountForFn == N, maxCountInAnyChain
61  // == N) become meaningful.
62  ContextHandle_t mainCtxtHndl = 0;
63 };
64 
65 static TLS_KEY g_tlsKey;
66 static inline TData* GetTls(THREADID t) {
67  return static_cast<TData*>(PIN_GetThreadData(g_tlsKey, t));
68 }
69 
70 static VOID ThreadStart(THREADID t, CONTEXT*, INT32, VOID*) {
71  PIN_SetThreadData(g_tlsKey, new TData(), t);
72 }
73 
74 // Track threads that have been observed so Fini can walk each one.
75 // Vector is populated single-threaded from ThreadStart under a lock;
76 // the victims are single-threaded so this is trivially safe.
77 static PIN_LOCK g_lock;
78 static vector<THREADID> g_threads;
79 
80 static VOID ThreadStartRegister(THREADID t, CONTEXT* ctxt, INT32 flags, VOID* v) {
81  ThreadStart(t, ctxt, flags, v);
82  PIN_GetLock(&g_lock, t + 1);
83  g_threads.push_back(t);
84  PIN_ReleaseLock(&g_lock);
85 }
86 
87 // Address range of main() in the app image, computed once at
88 // image-load time. Used in InstrumentInsCallback to decide whether
89 // a given instrumented ins belongs to main -- if so, we also insert
90 // a CaptureMainHandle callback, which grabs main's ctxt handle from
91 // the first call/ret slot INSIDE main. RTN_InsertCall at IPOINT_BEFORE
92 // of main wouldn't work reliably: main's entry Pin trace often has
93 // zero call/ret slots (e.g. a for-loop-body function with no calls
94 // until fprintf near the end), and GetContextHandle(t, 0) on a
95 // zero-slot trace returns 0 -- indistinguishable from "not captured".
96 static ADDRINT g_mainLo = 0, g_mainHi = 0;
97 
98 // ---------------- Analysis + instrumentation ----------------------
99 
100 static VOID RecordCtxt(uint32_t opaqueHandle, THREADID t) {
101  uint32_t h = GetContextHandle(t, opaqueHandle);
102  if (h == 0)
103  return; // cctlib's uninitialized-handle sentinel
104  GetTls(t)->hits[h]++;
105 }
106 
107 // Fires at every instrumented (call/ret) slot inside main's address
108 // range. The FIRST call that grabs a nonzero handle stashes it as
109 // main's ctxt handle in per-thread TLS -- subsequent calls are no-ops.
110 // Guard-order matters: we take the check on the fast path so post-
111 // capture invocations bail early.
112 static VOID CaptureMainHandle(uint32_t opaqueHandle, THREADID t) {
113  TData* td = GetTls(t);
114  if (!td || td->mainCtxtHndl != 0)
115  return;
116  uint32_t h = GetContextHandle(t, opaqueHandle);
117  if (h == 0)
118  return;
119  td->mainCtxtHndl = h;
120 }
121 
122 static VOID InstrumentInsCallback(INS ins, VOID*, const uint32_t slot) {
123  INS_InsertPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)RecordCtxt,
124  IARG_UINT32, slot, IARG_THREAD_ID, IARG_END);
125  // If this slot lives inside main, also insert a CaptureMainHandle
126  // callback so the first nonzero handle seen from within main is
127  // stashed as the sub-inventory's slice point.
128  if (g_mainLo != 0 && INS_Address(ins) >= g_mainLo && INS_Address(ins) < g_mainHi) {
129  INS_InsertPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)CaptureMainHandle,
130  IARG_UINT32, slot, IARG_THREAD_ID, IARG_END);
131  }
132 }
133 
134 // Call/ret-only filter. cctlib assigns a slot to every CALL and RET
135 // regardless of the isInterestingIns callback (see
136 // PopulateIPReverseMapAndAccountTraceInstructions in cctlib.cpp), and
137 // invokes the user's InstrumentInsCallback for CALL/RET only when
138 // isInterestingIns(ins) returns true. Returning true here for
139 // CALL/RET is the leanest way to get RecordCtxt fired at exactly the
140 // call/ret slots that mark trace boundaries -- one such handle per
141 // reached TraceNode is sufficient for shape assertions (GetFullCallingContext
142 // from any handle in a trace gives the full chain). Skips memory
143 // instrumentation entirely -- shape tests don't need it, and it was
144 // dominating runtime on exception victims with 5000-throw loops.
145 inline BOOL InterestingInsCallOrRet(INS ins) {
146  return INS_IsProcedureCall(ins) || INS_IsRet(ins);
147 }
148 
149 // Find "main" in the main executable image and record its address
150 // range so InstrumentInsCallback can steer CaptureMainHandle inserts.
151 static VOID OnImgLoad(IMG img, VOID*) {
152  if (!IMG_IsMainExecutable(img))
153  return;
154  RTN mainRtn = RTN_FindByName(img, "main");
155  if (!RTN_Valid(mainRtn))
156  return;
157  g_mainLo = RTN_Address(mainRtn);
158  g_mainHi = g_mainLo + RTN_Size(mainRtn);
159 }
160 
161 // ---------------- CCT inventory ------------------------------------
162 
163 using FnChain = vector<string>;
164 
165 struct CctInventory {
166  // Distinct root-to-leaf function-name chain -> aggregate hit count.
167  map<FnChain, uint64_t> chains;
168  // Leaf function name -> set of distinct chains ending there.
169  map<string, set<FnChain>> byLeafFn;
170  // Leaf function name -> aggregate hit count.
171  map<string, uint64_t> hitsByLeafFn;
172  // If any cctlib HARD sentinel appears anywhere, record what and how many.
173  // Soft PLT sentinels are recorded separately for diagnostics.
174  map<string, size_t> sentinelCounts;
175  map<string, size_t> softSentinelCounts;
176  size_t maxDepthObserved = 0;
177  size_t totalDistinctHandles = 0;
178 
179  // Sub-inventory: only chains that pass through main's TraceNode,
180  // sliced to start at main (pre-main libc/loader ancestors stripped).
181  // Populated when the walker was able to identify main's ctxt
182  // handle. Assertions targeting recursion or user-level structure
183  // should use these methods so pre-main clutter and any orphan
184  // chains (e.g., from cctlib initialization) don't dilute the
185  // signal.
186  map<FnChain, uint64_t> subChains;
187  map<string, set<FnChain>> subByLeafFn;
188  size_t subMaxDepthObserved = 0;
189 
190  size_t chainCountForFn(const string& fn) const {
191  auto it = byLeafFn.find(fn);
192  return it == byLeafFn.end() ? 0 : it->second.size();
193  }
194  bool hasChain(const vector<string>& expected) const {
195  return chains.count(expected) > 0;
196  }
197  bool hasFn(const string& fn) const {
198  return byLeafFn.count(fn) > 0;
199  }
200  bool anyChainContainsFn(const string& fn) const {
201  for (const auto& kv : chains) {
202  for (const auto& n : kv.first)
203  if (n == fn)
204  return true;
205  }
206  return false;
207  }
208  // True iff for every chain ending in leafFn, the immediate parent
209  // (chain[size-2]) is expectedParent. Empty parent means we found
210  // no chain with a parent at all, which we treat as false.
211  bool everyChainToFnHasImmediateParent(const string& leafFn,
212  const string& expectedParent) const {
213  auto it = byLeafFn.find(leafFn);
214  if (it == byLeafFn.end() || it->second.empty())
215  return false;
216  return std::all_of(it->second.begin(), it->second.end(),
217  [&](const FnChain& chain) {
218  return chain.size() >= 2 && chain[chain.size() - 2] == expectedParent;
219  });
220  }
221  // Max number of times `fn` appears in any single chain. For a
222  // routine we've direct-self-recursion-collapsed this is 1 (fn
223  // is only the leaf, never its own ancestor). For an uncollapsed
224  // recursive routine (indirect recursion, or if collapse regressed)
225  // it grows with the recursion depth.
226  size_t maxCountInAnyChain(const string& fn) const {
227  size_t maxC = 0;
228  for (const auto& kv : chains) {
229  size_t c = 0;
230  for (const auto& n : kv.first)
231  if (n == fn)
232  ++c;
233  if (c > maxC)
234  maxC = c;
235  }
236  return maxC;
237  }
238 
239  // ---- Sub-inventory (under main) mirrors ---------------------
240 
241  size_t subChainCountForFn(const string& fn) const {
242  auto it = subByLeafFn.find(fn);
243  return it == subByLeafFn.end() ? 0 : it->second.size();
244  }
245  bool subHasFn(const string& fn) const {
246  return subByLeafFn.count(fn) > 0;
247  }
248  bool subAnyChainContainsFn(const string& fn) const {
249  for (const auto& kv : subChains) {
250  for (const auto& n : kv.first)
251  if (n == fn)
252  return true;
253  }
254  return false;
255  }
256  size_t subMaxCountInAnyChain(const string& fn) const {
257  size_t maxC = 0;
258  for (const auto& kv : subChains) {
259  size_t c = 0;
260  for (const auto& n : kv.first)
261  if (n == fn)
262  ++c;
263  if (c > maxC)
264  maxC = c;
265  }
266  return maxC;
267  }
268  // Total distinct sub-chains that contain fn anywhere (leaf OR
269  // ancestor). For the "how many times does this function appear
270  // across the sub-CCT" question independent of whether it's a leaf.
271  size_t subChainsContainingFn(const string& fn) const {
272  size_t n = 0;
273  for (const auto& kv : subChains) {
274  for (const auto& e : kv.first)
275  if (e == fn) {
276  ++n;
277  break;
278  }
279  }
280  return n;
281  }
282  // True iff for every sub-chain ending in leafFn the immediate
283  // parent is expectedParent.
284  bool everySubChainToFnHasImmediateParent(const string& leafFn,
285  const string& expectedParent) const {
286  auto it = subByLeafFn.find(leafFn);
287  if (it == subByLeafFn.end() || it->second.empty())
288  return false;
289  return std::all_of(it->second.begin(), it->second.end(),
290  [&](const FnChain& chain) {
291  return chain.size() >= 2 && chain[chain.size() - 2] == expectedParent;
292  });
293  }
294 
295  // True iff every chain containing `fn` at any position has NO
296  // ancestor (chain[0..pos-1]) whose function name is in `badAncestors`.
297  // Used to assert that a marker in a catch/try body is not rooted
298  // under __cxa_throw / _Unwind_* / __gxx_personality_v0 (which would
299  // indicate cctlib mis-anchored the landing pad).
300  //
301  // Returns true when `fn` never appears (vacuously true is not what
302  // we want -- caller should first assert `hasFn(fn)`).
303  bool noAncestorOfFnIsInSet(const string& fn,
304  const set<string>& badAncestors) const {
305  for (const auto& kv : chains) {
306  const auto& chain = kv.first;
307  for (size_t i = 0; i < chain.size(); ++i) {
308  if (chain[i] != fn)
309  continue;
310  for (size_t j = 0; j < i; ++j) {
311  if (badAncestors.count(chain[j]))
312  return false;
313  }
314  }
315  }
316  return true;
317  }
318 };
319 
320 // cctlib sentinel function-names produced by GetFullCallingContext when
321 // it can't render a frame.
322 //
323 // HARD sentinels indicate cctlib silently degraded during the walk
324 // (BAD IP: address doesn't map to any loaded image; CRASHED: cctlib
325 // caught SIGSEGV during the walk; FAILED_TO_READ: postmortem lookup
326 // failed). Their appearance in a shape-check is a real bug.
327 //
328 // SOFT sentinels ("IN PLT BUT NOT VALID GOT", "UNRECOGNIZED PLT
329 // SIGNATURE") are cctlib's fallbacks when its ad-hoc PLT-slot
330 // pattern-match fails on the resolver stub -- a pre-existing
331 // limitation of cctlib's symbolication for modern glibc PLT
332 // layouts, unrelated to CCT-shape correctness. Similarly,
333 // "Truncated call path (due to deep call chain)" only means the
334 // walk hit cctlib's MAX_CCT_PRINT_DEPTH=20 rendering limit; the
335 // underlying CCT is fine. Deep C++-runtime unwind stacks
336 // (__cxa_throw -> __gxx_personality_v0 -> _Unwind_RaiseException
337 // -> ...) routinely exceed 20 frames on exception victims. Soft
338 // sentinels are counted separately for diagnostics but must NOT
339 // fail the test.
340 //
341 // THREAD[n]_ROOT_CTXT is normal and skipped (isRootName).
342 static bool isHardSentinelName(const string& n) {
343  return n == "CRASHED !!" ||
344  n == "BAD IP !!" ||
345  n == "FAILED_TO_READ" ||
346  n == "CRASHED!";
347 }
348 
349 static bool isSoftSentinelName(const string& n) {
350  return n == "IN PLT BUT NOT VALID GOT" ||
351  n == "UNRECOGNIZED PLT SIGNATURE" ||
352  n == "Truncated call path (due to deep call chain)";
353 }
354 
355 static bool isSentinelName(const string& n) {
356  return isHardSentinelName(n) || isSoftSentinelName(n);
357 }
358 
359 static bool isRootName(const string& n) {
360  // The literal form is "THREAD[<id>]_ROOT_CTXT" (cctlib.cpp:2119).
361  return n.rfind("THREAD[", 0) == 0 && n.find("]_ROOT_CTXT") != string::npos;
362 }
363 
364 static void BuildInventoryFromThread(THREADID t, CctInventory& inv) {
365  auto* td = GetTls(t);
366  if (!td)
367  return;
368  inv.totalDistinctHandles += td->hits.size();
369 
370  // Canonicalize main's TraceNode as a "trace-start handle" using
371  // the new public helper GetTraceStartHandle (cctlib.H). Two
372  // ContextHandles from the same TraceNode produce the same
373  // trace-start handle. Below we walk each chain leaf->root and
374  // slice at the position whose trace-start matches main's; that
375  // slice becomes the sub-chain rooted at main.
376  ContextHandle_t mainTraceStart =
377  (td->mainCtxtHndl != 0) ? GetTraceStartHandle(td->mainCtxtHndl) : 0;
378 
379  for (const auto& kv : td->hits) {
380  uint32_t h = kv.first;
381  uint64_t hits = kv.second;
382  vector<Context> chain;
384  // chain is leaf-first. Two passes:
385  // (1) Collect ALL user-frame names for the full-CCT inventory.
386  // (2) For the sub-inventory, note the FIRST chain position
387  // (walking leaf->root, i.e. lowest index) whose Context
388  // lives in main's TraceNode. That marks the outermost
389  // main-frame. Everything from index 0 up to that
390  // position (inclusive) is the sub-chain rooted at main.
391  // (Note "outermost" from a leaf-first walk = closest to
392  // root = highest index with a main match. But since main
393  // is called exactly once from libc startup, there's only
394  // one match in a well-formed chain -- see the pre-existing
395  // exception-hook bug note below for the pathological
396  // multi-main case.)
397  FnChain names;
398  names.reserve(chain.size());
399  int mainIdxLeafFirst = -1;
400  for (size_t i = 0; i < chain.size(); ++i) {
401  auto& c = chain[i];
402  if (isRootName(c.functionName))
403  continue;
404  if (isHardSentinelName(c.functionName)) {
405  inv.sentinelCounts[c.functionName] += 1;
406  }
407  if (isSoftSentinelName(c.functionName)) {
408  inv.softSentinelCounts[c.functionName] += 1;
409  }
410  names.push_back(c.functionName);
411  // Match against main's TraceNode using the trace-start
412  // helper. We want the OUTERMOST (deepest ancestor) match,
413  // which is the LAST such index walking leaf->root.
414  if (mainTraceStart != 0 &&
415  GetTraceStartHandle(c.ctxtHandle) == mainTraceStart) {
416  mainIdxLeafFirst = (int)(names.size() - 1);
417  }
418  }
419  if (names.empty())
420  continue; // pure-root chain, nothing useful
421 
422  // Reverse to root-first for the full-inventory signature.
423  FnChain sig(names.rbegin(), names.rend());
424  if (sig.size() > inv.maxDepthObserved)
425  inv.maxDepthObserved = sig.size();
426  inv.chains[sig] += hits;
427  const string& leafFn = sig.back();
428  inv.byLeafFn[leafFn].insert(sig);
429  inv.hitsByLeafFn[leafFn] += hits;
430 
431  // Sub-inventory: only if this chain went through main.
432  // mainIdxLeafFirst is an index into `names` (leaf-first). The
433  // sub-chain leaf-first is names[0..mainIdxLeafFirst] inclusive.
434  // Reverse to get root-first sub-chain starting at main.
435  if (mainIdxLeafFirst >= 0) {
436  FnChain subLeafFirst(names.begin(),
437  names.begin() + mainIdxLeafFirst + 1);
438  FnChain subSig(subLeafFirst.rbegin(), subLeafFirst.rend());
439  if (subSig.size() > inv.subMaxDepthObserved) {
440  inv.subMaxDepthObserved = subSig.size();
441  }
442  inv.subChains[subSig] += hits;
443  const string& subLeaf = subSig.back();
444  inv.subByLeafFn[subLeaf].insert(subSig);
445  }
446  }
447 }
448 
449 // ---------------- Assertion recorder -------------------------------
450 
452  vector<string> failures;
453  const char* checkName;
454 
455  template <typename A, typename B>
456  void expectLE(A obs, B lim, const char* what) {
457  if (!(obs <= (A)lim)) {
458  ostringstream os;
459  os << "FAIL " << checkName << ": " << what
460  << " expected <= " << lim << " observed " << obs;
461  failures.push_back(os.str());
462  }
463  }
464  template <typename A, typename B>
465  void expectGE(A obs, B lim, const char* what) {
466  if (!(obs >= (A)lim)) {
467  ostringstream os;
468  os << "FAIL " << checkName << ": " << what
469  << " expected >= " << lim << " observed " << obs;
470  failures.push_back(os.str());
471  }
472  }
473  template <typename A, typename B, typename C>
474  void expectInRange(A obs, B lo, C hi, const char* what) {
475  if (!(obs >= (A)lo && obs <= (A)hi)) {
476  ostringstream os;
477  os << "FAIL " << checkName << ": " << what
478  << " expected in [" << lo << "," << hi << "] observed " << obs;
479  failures.push_back(os.str());
480  }
481  }
482  template <typename A, typename B>
483  void expectEQ(A obs, B want, const char* what) {
484  if (!(obs == (A)want)) {
485  ostringstream os;
486  os << "FAIL " << checkName << ": " << what
487  << " expected == " << want << " observed " << obs;
488  failures.push_back(os.str());
489  }
490  }
491  void expectTrue(bool b, const char* what) {
492  if (!b) {
493  ostringstream os;
494  os << "FAIL " << checkName << ": " << what << " (expected true)";
495  failures.push_back(os.str());
496  }
497  }
498  void expectNoSentinels(const CctInventory& inv) {
499  if (!inv.sentinelCounts.empty()) {
500  ostringstream os;
501  os << "FAIL " << checkName << ": sentinel frames appeared:";
502  for (const auto& kv : inv.sentinelCounts) {
503  os << " [" << kv.first << " x" << kv.second << "]";
504  }
505  failures.push_back(os.str());
506  }
507  }
508 };
509 
510 // The set of function names an in-try or in-catch MARKER function must
511 // NEVER have as an ancestor in any chain. If a marker is descended from
512 // __cxa_throw / __gxx_personality_v0 / _Unwind_*, cctlib mis-anchored
513 // the landing pad and the catch/try body's call is attributed to the
514 // throw-machinery subtree instead of the function that owns the
515 // try/catch.
516 static const set<string> kThrowMachineryFns = {
517  "__cxa_throw",
518  "__cxa_rethrow",
519  "__cxa_allocate_exception",
520  "__cxa_begin_catch",
521  "__cxa_end_catch",
522  "__gxx_personality_v0",
523  "_Unwind_RaiseException",
524  "_Unwind_Resume",
525  "_Unwind_Resume_or_Rethrow",
526  "_Unwind_ForcedUnwind",
527  "_Unwind_Backtrace",
528  "_Unwind_Find_FDE",
529  "_Unwind_GetIP",
530  "_Unwind_GetTextRelBase",
531  "_Unwind_GetCFA",
532  "__longjmp",
533 };
534 
535 // Assert that a marker function `fn` is (a) present in the CCT,
536 // (b) whose immediate parent in every chain equals `parent`,
537 // (c) never has any throw/unwind-machinery function as an ancestor.
538 // The third check is the load-bearing one: prior to the landing-pad
539 // re-anchor fix, in-catch markers would appear as descendants of
540 // __cxa_throw's subtree instead of children of the frame owning the
541 // try/catch.
543  const CctInventory& inv,
544  const string& fn,
545  const string& parent) {
546  r.expectTrue(inv.hasFn(fn), (fn + " appears in the CCT").c_str());
548  (fn + "'s immediate parent is " + parent).c_str());
550  (fn + " has NO ancestor in the throw/unwind machinery "
551  "(catch/try body must not be rooted under __cxa_throw)")
552  .c_str());
553 }
554 
555 // ---------------- Per-victim check functions -----------------------
556 //
557 // Recursion victims. Depths are sized so uncollapsed chains stay under
558 // MAX_CCT_PRINT_DEPTH=20 -- otherwise cctlib appends a Truncated
559 // sentinel that would degrade the sensitivity of these assertions.
560 
561 static void check_rec_fib_deep(const CctInventory& inv, AssertionRecorder& r) {
562  // fib(15) with direct-self-recursion collapse. In the CCT rooted
563  // at main, there is EXACTLY ONE fib TraceNode:
564  // * Exactly 1 distinct sub-chain ends in fib: [main, fib].
565  // * fib appears EXACTLY ONCE along any path from main to leaf.
566  // Without collapse these would grow to 15 and 15 respectively.
567  r.expectTrue(inv.subHasFn("fib"), "fib appears as a leaf function under main");
568  r.expectEQ(inv.subChainCountForFn("fib"), (size_t)1,
569  "sub-CCT under main has exactly 1 distinct chain ending in fib");
570  r.expectEQ(inv.subMaxCountInAnyChain("fib"), (size_t)1,
571  "fib appears at most once along any path from main to leaf");
572  r.expectTrue(inv.everySubChainToFnHasImmediateParent("fib", "main"),
573  "fib's immediate parent in every sub-chain is main");
574  r.expectNoSentinels(inv);
575 }
576 
578  // A(3,4). Same collapse story as fib.
579  r.expectTrue(inv.subHasFn("A"), "A appears as a leaf function under main");
580  r.expectEQ(inv.subChainCountForFn("A"), (size_t)1,
581  "sub-CCT under main has exactly 1 distinct chain ending in A");
582  r.expectEQ(inv.subMaxCountInAnyChain("A"), (size_t)1,
583  "A appears at most once along any path from main to leaf");
585  "A's immediate parent in every sub-chain is main");
586  r.expectNoSentinels(inv);
587 }
588 
590  // multi(15) with THREE static direct self-call sites, all
591  // collapse into one frame under main.
592  r.expectTrue(inv.subHasFn("multi"), "multi appears as a leaf function under main");
593  r.expectEQ(inv.subChainCountForFn("multi"), (size_t)1,
594  "sub-CCT under main has exactly 1 distinct chain ending in multi "
595  "(all 3 direct self-call sites collapsed into one physical frame)");
596  r.expectEQ(inv.subMaxCountInAnyChain("multi"), (size_t)1,
597  "multi appears at most once along any path from main to leaf");
598  r.expectTrue(inv.everySubChainToFnHasImmediateParent("multi", "main"),
599  "multi's immediate parent in every sub-chain is main");
600  r.expectNoSentinels(inv);
601 }
602 
604  // indirect_rec(12) via function pointer. Called normally from
605  // main once (indirect_rec(12)), then makes 12 successive indirect
606  // self-calls -> indirect_rec(11), (10), ..., (0). Total 13
607  // physical frames of indirect_rec (n from 12 down to 0, inclusive).
608  //
609  // Design deliberately does NOT collapse indirect self-recursion,
610  // so every activation is a fresh physical frame:
611  // * indirect_rec appears EXACTLY 13 times along the deepest
612  // path (13 nested frames of indirect_rec).
613  // * subChainCountForFn("indirect_rec") == 13 (one distinct
614  // chain per depth level -- leaf at each of the 13 frames).
615  // If either drops below 13, we've accidentally collapsed indirect
616  // recursion (the explicit non-goal of the design).
617  r.expectTrue(inv.subHasFn("indirect_rec"), "indirect_rec appears under main");
618  r.expectEQ(inv.subChainCountForFn("indirect_rec"), (size_t)13,
619  "sub-CCT under main has exactly 13 distinct chains ending in indirect_rec "
620  "(1 initial direct call from main + 12 indirect frames, no collapse)");
621  r.expectEQ(inv.subMaxCountInAnyChain("indirect_rec"), (size_t)13,
622  "indirect_rec appears exactly 13 times along the deepest path from main");
623  r.expectNoSentinels(inv);
624 }
625 
627  // mixed(30): every 5th activation is indirect (creates a new
628  // frame), the rest are direct (collapse into the current indirect
629  // frame). Indirect calls happen at n = 30, 25, 20, 15, 10, 5
630  // (n%5==0) -> 6 indirect frames plus the initial normal call
631  // from main = 7 mixed frames total.
632  // * mixed appears EXACTLY 7 times along the deepest path.
633  // * subChainCountForFn("mixed") == 7 (one distinct chain per depth).
634  r.expectTrue(inv.subHasFn("mixed"), "mixed appears under main");
635  r.expectEQ(inv.subChainCountForFn("mixed"), (size_t)7,
636  "sub-CCT under main has exactly 7 distinct chains ending in mixed "
637  "(6 indirect frames + 1 initial call, direct sites collapse within each)");
638  r.expectEQ(inv.subMaxCountInAnyChain("mixed"), (size_t)7,
639  "mixed appears exactly 7 times along the deepest path from main");
640  r.expectNoSentinels(inv);
641 }
642 
643 static void check_rec_stripped(const CctInventory& inv, AssertionRecorder& r) {
644  // Stripped binary. Pin identifies the whole .text as one apparent
645  // routine, so the recursion classifier never sees fib's self-call
646  // and collapse can't engage (Pin-side routine granularity issue,
647  // not a cctlib limitation we can fix). Additionally: main's
648  // symbol is stripped, so our OnImgLoad hook's RTN_FindByName(img,
649  // "main") returns invalid -- mainCtxtHndl is never set and the
650  // sub-inventory is empty. Fall back to full-CCT assertions.
651  r.expectTrue(inv.hasFn("") || inv.hasFn(".text"),
652  "at least one app-side leaf attributed (empty-name or .text)");
653  r.expectNoSentinels(inv);
654 }
655 
657  // descend(15) is direct self-recursive; throw at leaf, main
658  // catches. Same collapse story as rec_fib_deep -- but under a
659  // throw path, so this exercises the collapse-plus-unwind
660  // interaction on the sub-CCT rooted at main.
661  r.expectTrue(inv.subHasFn("descend"), "descend appears under main");
662  r.expectEQ(inv.subChainCountForFn("descend"), (size_t)1,
663  "sub-CCT under main has exactly 1 distinct chain ending in descend "
664  "(collapse holds under a throw path)");
665  r.expectEQ(inv.subMaxCountInAnyChain("descend"), (size_t)1,
666  "descend appears at most once along any path from main");
667  r.expectTrue(inv.everySubChainToFnHasImmediateParent("descend", "main"),
668  "descend's immediate parent in every sub-chain is main");
669  r.expectNoSentinels(inv);
670 }
671 
673  // Non-recursive baseline. Sanity: sub-CCT under main exists.
674  r.expectGE(inv.subChains.size(), (size_t)1, "at least one sub-chain under main");
675  r.expectNoSentinels(inv);
676 }
677 
678 // Exception victims. Load-bearing invariant: cctlib's unwind path
679 // must leave the CCT coherent -- no BAD IP, no chain corruption of
680 // the pre-throw path, no post-catch code stranded under the throwing
681 // frame's fresh subtree.
682 //
683 // !!! PRE-EXISTING CCTLIB BUG SURFACED BY THESE TESTS !!!
684 // cctlib's exception hook fires only on _Unwind_RaiseException's
685 // last RET (src/cctlib.cpp:3033-3035). But when an exception is
686 // CAUGHT, _Unwind_RaiseException never returns -- libgcc does a
687 // context-restore directly into the handler's landing pad. So
688 // SetCurTraceNodeAfterExceptionIfContextIsInstalled never fires,
689 // tlsCurrentTraceNode stays deep in libgcc, and the landing pad's
690 // Pin trace enters InstrumentTraceEntry with a stale CCT anchor.
691 // Result: iteration N of a try/catch loop creates a NEW landing-pad
692 // TraceNode as a child of iter N-1's __cxa_throw subtree, growing
693 // the chain unboundedly. See exc_deep_unwind chain dump for the
694 // signature (main;recurse;__cxa_throw;main;recurse;__cxa_throw;...).
695 // Existing exception_integration_test never noticed because it only
696 // checks "tool exits cleanly + produces a report".
697 //
698 // The assertions below intentionally do NOT trip on this bug -- they
699 // check the invariants that DO hold today (parent of the recursive
700 // function is main; no BAD IP; etc.). A separate cctlib fix would
701 // enable the tighter forms of these checks (marked "TIGHTER WITH FIX").
702 // TODO(cctlib-exception-hook): fix by attaching the reset to a hook
703 // that fires regardless of whether _Unwind_RaiseException returns
704 // normally, then re-enable the tighter checks.
705 
707  // Structure: main { for i in ITERS { try { recurse(D, i); } catch {} } }
708  // recurse() is DIRECT self-recursive; deepest activation throws.
709  // Now that cctlib properly re-anchors to main's frame on each
710  // landing-pad delivery, recurse never appears as its own ancestor
711  // -- the collapse holds cleanly across every unwind cycle.
712  expectMarkerAnchored(r, inv, "deep_try_marker", "main");
713  expectMarkerAnchored(r, inv, "deep_catch_marker", "main");
714  r.expectTrue(inv.hasFn("recurse"), "recurse appears as a leaf function");
715  r.expectTrue(inv.everyChainToFnHasImmediateParent("recurse", "main"),
716  "recurse's immediate parent is always main (recurse fully collapses even under throw)");
717  r.expectEQ(inv.maxCountInAnyChain("recurse"), (size_t)1,
718  "recurse never appears as ancestor of recurse (landing-pad re-anchor holds)");
719  r.expectNoSentinels(inv);
720 }
721 
723  // Structure: throw fires mid-ctor; partial dtors unwind; main catches.
724  // Multi-phase unwind exercises both a cleanup landing pad (Wrap
725  // ctor's dtor cleanup for A) and a handler landing pad (main's
726  // catch). Both re-anchor via the _Unwind_SetIP-driven hook.
727  // Landing-pad Pin traces get function-labeled with the enclosing
728  // function's own name (their first insn is inside the function
729  // body), so `main` may appear up to twice in a chain: main's own
730  // entry trace + main's landing-pad Pin trace.
731  expectMarkerAnchored(r, inv, "ctorthrow_try_marker", "main");
732  expectMarkerAnchored(r, inv, "ctorthrow_catch_marker", "main");
733  r.expectTrue(inv.anyChainContainsFn("main"),
734  "main still reachable after the ctor throw");
735  r.expectLE(inv.maxCountInAnyChain("main"), (size_t)4,
736  "main appears at most a small bounded number of times (bug pre-fix: unbounded)");
737  r.expectNoSentinels(inv);
738 }
739 
741  // Structure:
742  // main { for i in ITERS {
743  // resume_after_catch(i) { try { may_throw(i); } catch(int) {} }
744  // post_catch_worker(i);
745  // }}
746  //
747  // Structural invariants:
748  // * may_throw's immediate parent is resume_after_catch (or a
749  // variant labeled resume_after_catch -- landing pads inside
750  // resume_after_catch get the same function name).
751  // * post_catch_worker's immediate parent is main (or main-
752  // labeled landing pad, which reads as `main` in chain fn-names).
753  // KEY signal: cctlib restored the anchor for the post-catch
754  // call, so C() is NOT stranded deep in the throwing subtree.
755  // * resume_try_marker and resume_catch_marker are both direct
756  // children of resume_after_catch and neither is descended from
757  // __cxa_throw.
758  expectMarkerAnchored(r, inv, "resume_try_marker", "resume_after_catch");
759  expectMarkerAnchored(r, inv, "resume_catch_marker", "resume_after_catch");
760  r.expectTrue(inv.everyChainToFnHasImmediateParent("may_throw", "resume_after_catch"),
761  "may_throw's immediate parent in every chain is resume_after_catch");
762  r.expectTrue(inv.everyChainToFnHasImmediateParent("post_catch_worker", "main"),
763  "post_catch_worker's immediate parent in every chain is main "
764  "(NOT resume_after_catch or may_throw)");
765  r.expectNoSentinels(inv);
766 }
767 
768 static void check_sig_longjmp(const CctInventory& inv, AssertionRecorder& r) {
769  // Structure: main { for i in ITERS { setjmp(jb); if 0: go_deep(i,8); } }
770  // go_deep is DIRECT self-recursive; deepest calls longjmp back to main.
771  // cctlib has its own setjmp/longjmp hooks (CaptureSigSetJmpCtxt /
772  // RestoreSigLongJmpCtxt / HoldLongJmpBuf) -- separate path from
773  // the exception hook -- but the same anchor-restoration invariant
774  // applies. Both markers must be direct children of main.
775  expectMarkerAnchored(r, inv, "sjlj_try_marker", "main");
776  expectMarkerAnchored(r, inv, "sjlj_landing_marker", "main");
777  r.expectTrue(inv.hasFn("go_deep"), "go_deep appears as a leaf function");
778  r.expectTrue(inv.everyChainToFnHasImmediateParent("go_deep", "main"),
779  "go_deep's immediate parent is always main");
780  r.expectNoSentinels(inv);
781 }
782 
784  // Structure: main { for i in ITERS { sigsetjmp; if 0: poke(i); } }
785  // poke(i) deref 0x1 -> SIGSEGV -> handler{siglongjmp back}.
786  //
787  // Under call/ret-only instrumentation, poke's Pin trace ends at
788  // its epilogue ret; the SIGSEGV fires BEFORE that ret ever
789  // executes, so no RecordCtxt call inside poke ever runs and poke
790  // won't appear in the recorded chains. handler DOES appear (it
791  // calls siglongjmp -- a slotted control-flow ins that fires
792  // before the longjmp). So we assert on handler + main + markers.
793  expectMarkerAnchored(r, inv, "sigsegv_try_marker", "main");
794  expectMarkerAnchored(r, inv, "sigsegv_recover_marker", "main");
795  r.expectTrue(inv.hasFn("handler"), "signal handler appears in the CCT");
796  r.expectTrue(inv.subAnyChainContainsFn("main"),
797  "main still reachable under itself after SIGSEGV+siglongjmp");
798  r.expectNoSentinels(inv);
799 }
800 
801 // -----------------------------------------------------------------------------
802 // Additional exception-victim shape checks. Each encodes the specific
803 // try/catch/throw structure of its victim so a regression in cctlib's
804 // unwind handling that misplaces a landing pad (or fails to fire the
805 // re-anchor) shows up as a specific parent-child mismatch, not as a
806 // silent CCT corruption.
807 // -----------------------------------------------------------------------------
808 
810  // Structure: main -> outer -> middle -> inner -> throw; main catches.
811  // Every-chain-parent invariants for the pre-throw call stack.
812  // simple_try_marker fires in the try body BEFORE outer(i) is called;
813  // simple_catch_marker fires in the catch body. Both must be direct
814  // children of main -- regressions manifest as either marker attaching
815  // as a descendant of __cxa_throw.
816  expectMarkerAnchored(r, inv, "simple_try_marker", "main");
817  expectMarkerAnchored(r, inv, "simple_catch_marker", "main");
818  r.expectTrue(inv.everyChainToFnHasImmediateParent("inner", "middle"),
819  "inner's immediate parent is middle");
820  r.expectTrue(inv.everyChainToFnHasImmediateParent("middle", "outer"),
821  "middle's immediate parent is outer");
822  r.expectTrue(inv.everyChainToFnHasImmediateParent("outer", "main"),
823  "outer's immediate parent is main (unwind didn't strand it)");
824  r.expectNoSentinels(inv);
825 }
826 
827 static void check_exc_rethrow(const CctInventory& inv, AssertionRecorder& r) {
828  // Structure:
829  // main { try { inner(i); } catch (std::exception&) { ... } }
830  // inner { try { raise_it(i); } catch (std::exception& e) { throw; } }
831  // raise_it { throw std::runtime_error(...); }
832  // Two _Unwind_SetIP call chains per iter: (1) raise_it -> inner's
833  // catch handler landing pad, (2) inner's `throw;` ->
834  // _Unwind_Resume_or_Rethrow -> main's catch handler landing pad.
835  // Four markers cover all four try/catch bodies -- each must be a
836  // direct child of its enclosing function AND not descended from
837  // any throw/unwind-machinery function.
838  expectMarkerAnchored(r, inv, "rethrow_outer_try_marker", "main");
839  expectMarkerAnchored(r, inv, "rethrow_outer_catch_marker", "main");
840  expectMarkerAnchored(r, inv, "rethrow_inner_try_marker", "inner");
841  expectMarkerAnchored(r, inv, "rethrow_inner_catch_marker", "inner");
842  r.expectTrue(inv.hasFn("raise_it"), "raise_it appears as a leaf function");
843  r.expectTrue(inv.hasFn("inner"), "inner appears in the CCT");
844  r.expectTrue(inv.subAnyChainContainsFn("main"),
845  "main still reachable after nested rethrow");
846  r.expectNoSentinels(inv);
847 }
848 
849 static void check_exc_catchall(const CctInventory& inv, AssertionRecorder& r) {
850  // Structure: main { try { thrower(kind, i); } catch (...) {} }
851  // thrower throws one of {int, POD, std::string, polymorphic type}
852  // depending on `kind`; kind=3 (Virt with a virtual dtor) additionally
853  // exercises a cleanup landing pad inside thrower's body.
854  // Both markers must be direct children of main and NOT descended
855  // from __cxa_throw -- a regression would attribute the catch(...)
856  // body's marker call to the throw subtree.
857  expectMarkerAnchored(r, inv, "catchall_try_marker", "main");
858  expectMarkerAnchored(r, inv, "catchall_catch_marker", "main");
859  r.expectTrue(inv.hasFn("thrower"), "thrower appears as a leaf function");
860  r.expectTrue(inv.subAnyChainContainsFn("main"),
861  "main still reachable under itself after catch-all");
862  r.expectNoSentinels(inv);
863 }
864 
866  // Structure: main { try { thrower(i); } catch (int) {} }
867  // thrower constructs two local Guard objects whose dtors run during
868  // unwind (via a cleanup landing pad INSIDE thrower's body) before
869  // main's catch fires. Exercises the multi-landing-pad phase-2 path
870  // where personality installs a cleanup context, dtors execute, then
871  // _Unwind_Resume triggers the next SetIP for main's catch.
872  // Both markers must be direct children of main.
873  expectMarkerAnchored(r, inv, "dtorcleanup_try_marker", "main");
874  expectMarkerAnchored(r, inv, "dtorcleanup_catch_marker", "main");
875  r.expectTrue(inv.hasFn("thrower"), "thrower appears as a leaf function");
876  r.expectTrue(inv.subAnyChainContainsFn("main"),
877  "main still reachable under itself after cleanup unwind");
878  r.expectNoSentinels(inv);
879 }
880 
882  // Structure: main { for i in ITERS { try { thrower(v); } catch (uint64_t) {} } }
883  // High-iteration throw/catch stress. Same shape as simple but many
884  // iterations -- catches a regression where cctlib accumulates
885  // state per iteration (leaking TraceNodes or drifting the anchor).
886  expectMarkerAnchored(r, inv, "stress_try_marker", "main");
887  expectMarkerAnchored(r, inv, "stress_catch_marker", "main");
888  r.expectTrue(inv.everyChainToFnHasImmediateParent("thrower", "main"),
889  "thrower's immediate parent is main");
890  r.expectNoSentinels(inv);
891 }
892 
894  // Structure: main { try { thrower(i); } catch (const std::exception&) {} }
895  // thrower throws Base/Mid/Leaf (public std::exception subclasses).
896  // Personality's type-matching path is exercised; landing-pad shape
897  // is still the same top-level main-catches-thrower structure.
898  expectMarkerAnchored(r, inv, "poly_try_marker", "main");
899  expectMarkerAnchored(r, inv, "poly_catch_marker", "main");
900  r.expectTrue(inv.everyChainToFnHasImmediateParent("thrower", "main"),
901  "thrower's immediate parent is main");
902  r.expectNoSentinels(inv);
903 }
904 
906  // Structure: main { for i in ITERS {
907  // try { rec(D, i); } catch(int) { rectry_outer_catch(i); }
908  // }}
909  // rec(depth, i) is DIRECT self-recursive; every activation wraps its
910  // downward call in try{...}catch(int){rethrow}. At depth==0 the
911  // throw fires; the exception propagates through EVERY frame's catch
912  // on its way up, and main's outer catch stops it.
913  //
914  // Direct-self-recursion collapse fuses all rec activations into ONE
915  // TraceNode. The load-bearing invariants:
916  // 1. rec appears at most ONCE in any chain (collapse holds under
917  // throw/rethrow -- landing-pad re-anchor must not defeat it).
918  // 2. rectry_try_marker, rectry_deep_marker, rectry_catch_marker
919  // all have immediate parent = rec (the collapsed node) and
920  // NO ancestor in throw/unwind machinery.
921  // 3. rectry_outer_try, rectry_outer_catch have immediate parent
922  // = main (post-catch anchor restored at the outermost frame).
923  // 4. rectry_after_marker must NOT appear -- the code path is
924  // unreachable (every activation either throws or catches+
925  // rethrows). If it appears the compiler / cctlib has
926  // hallucinated an unreachable code path.
927  // Direct-self-recursion collapse fuses all rec activations into ONE
928  // TraceNode. But rec's catch-handler landing pad is a SEPARATE Pin
929  // trace whose first IP lies inside rec's body -- Pin labels it
930  // `rec` too via RTN_FindNameByAddress. So chain function-name
931  // sequences can show `rec -> rec` even with perfect collapse: the
932  // first `rec` is the collapsed TraceNode, the second is the
933  // catch-LP Pin trace. maxCountInAnyChain counts fn-name occurrences,
934  // not TraceNodes, so bound to 2 (entry + LP). > 2 would indicate a
935  // real collapse regression.
936  r.expectTrue(inv.hasFn("rec"), "rec appears in the CCT");
937  r.expectLE(inv.maxCountInAnyChain("rec"), (size_t)2,
938  "rec appears at most twice per chain (collapsed TraceNode + "
939  "catch-landing-pad Pin trace labeled 'rec' by RTN name lookup)");
940 
941  expectMarkerAnchored(r, inv, "rectry_try_marker", "rec");
942  expectMarkerAnchored(r, inv, "rectry_deep_marker", "rec");
943  expectMarkerAnchored(r, inv, "rectry_catch_marker", "rec");
944  expectMarkerAnchored(r, inv, "rectry_outer_try", "main");
945  expectMarkerAnchored(r, inv, "rectry_outer_catch", "main");
946 
947  r.expectTrue(!inv.hasFn("rectry_after_marker"),
948  "rectry_after_marker must NOT appear (unreachable code path)");
949  r.expectNoSentinels(inv);
950 }
951 
952 static void check_exc_none_tn(const CctInventory& inv, AssertionRecorder& r) {
953  // Baseline: no throws. Sanity: main appears; no HARD sentinels.
954  // The tool's exception machinery must be inert when no exception
955  // fires -- catches any regression where cctlib's _Unwind_SetIP
956  // hook fires spuriously or the pending-reset state is dirty on
957  // process startup.
958  r.expectTrue(inv.subAnyChainContainsFn("main"),
959  "main is reachable in the sub-CCT");
960  r.expectNoSentinels(inv);
961 }
962 
964  // Structure: main { thrower(); } thrower { throw; }
965  // Nobody catches. libgcc's phase-1 search returns _URC_END_OF_STACK
966  // -> phase 2 never entered -> _Unwind_SetIP never called -> our
967  // hook never fires -> pending state never armed. std::terminate
968  // is called and the custom terminator calls _exit(0). The tool
969  // must NOT crash on the uncaught-exception path (a pre-existing
970  // guard was there to prevent dereferencing a NULL exception-
971  // handler frame; the new design has no such state, so no guard
972  // is needed, but the invariant "cctlib survives" must still hold).
973  r.expectTrue(inv.hasFn("thrower"), "thrower appears as a leaf function");
974  r.expectTrue(inv.everyChainToFnHasImmediateParent("thrower", "main"),
975  "thrower's immediate parent is main");
976  r.expectNoSentinels(inv);
977 }
978 
979 // Dispatch table.
980 using CheckFn = void (*)(const CctInventory&, AssertionRecorder&);
981 static const map<string, CheckFn> kChecks = {
982  {"rec_fib_deep", check_rec_fib_deep},
983  {"rec_ackermann", check_rec_ackermann},
984  {"rec_multi_direct", check_rec_multi_direct},
985  {"rec_indirect_only", check_rec_indirect_only},
986  {"rec_mixed_direct_indirect", check_rec_mixed_direct_indirect},
987  {"rec_stripped", check_rec_stripped},
988  {"rec_exception", check_rec_exception},
989  {"rec_baseline_nonrec", check_rec_baseline_nonrec},
990  // Exception victims -- per-victim checks encoding the specific
991  // try/catch structure of each. All 13 victims are wired here;
992  // exception_shape_test.cpp selects the subset that runs (the
993  // stress victims are slow under Pin+cctlib so they may be
994  // gated behind an "EXPENSIVE" env var).
995  {"exc_simple_throw", check_exc_simple_throw},
996  {"exc_deep_unwind", check_exc_deep_unwind},
997  {"exc_rethrow", check_exc_rethrow},
998  {"exc_catchall", check_exc_catchall},
999  {"exc_dtor_cleanup", check_exc_dtor_cleanup},
1000  {"exc_stress_loop", check_exc_stress_loop},
1001  {"exc_polymorphic", check_exc_polymorphic},
1002  {"exc_recurse_trycatch", check_exc_recurse_trycatch},
1003  {"exc_none_tn", check_exc_none_tn},
1004  {"exc_uncaught_tn", check_exc_uncaught_tn},
1005  {"exc_ctor_throw", check_exc_ctor_throw},
1006  {"exc_catch_and_resume", check_exc_catch_and_resume},
1007  {"sig_longjmp", check_sig_longjmp},
1008  {"sig_sigsegv_recover", check_sig_sigsegv_recover},
1009 };
1010 
1011 // ---------------- Fini --------------------------------------------
1012 
1013 static void RunChecksAndExit(const string& check);
1014 
1015 // Fires when a thread finishes -- crucially, BEFORE Pin unloads any
1016 // images. cctlib's GetFullCallingContext calls IsValidIP internally,
1017 // which walks APP_ImgHead(); at PIN_AddFiniFunction time that list
1018 // is already empty and every frame comes back as "BAD IP !!". Doing
1019 // the walk here (single-threaded victims -> exactly one call) means
1020 // the target's own IPs are still in loaded-image ranges and resolve.
1021 // Deadspy dodges the same trap by walking at IMG_AddUnloadFunction
1022 // time (clients/deadspy_client.cpp:1551).
1023 // Shared flag: whichever of ThreadFini / FiniFunc fires first wins,
1024 // the other is a no-op. This matters because if the first-firing
1025 // callback's assertions all pass, RunChecksAndExit returns normally
1026 // -- the second callback would then re-walk with cctlib images
1027 // already unloaded and clobber the result with spurious BAD IP
1028 // failures.
1029 static bool g_checkRan = false;
1030 
1031 static VOID ThreadFini(THREADID t, const CONTEXT*, INT32, VOID*) {
1032  if (g_checkRan)
1033  return;
1034  g_checkRan = true;
1035  RunChecksAndExit(KnobCheck.Value());
1036 }
1037 
1038 static void RunChecksAndExit(const string& check) {
1039  if (check.empty()) {
1040  fprintf(stderr, "cct_shape_check: -check <name> is required\n");
1041  PIN_ExitProcess(2);
1042  }
1043  auto it = kChecks.find(check);
1044  if (it == kChecks.end()) {
1045  fprintf(stderr, "cct_shape_check: no check registered for '%s'\n", check.c_str());
1046  PIN_ExitProcess(2);
1047  }
1048 
1049  // Debug: dump the first 5 recorded handles' walks via cctlib's own
1050  // PrintFullCallingContext to compare against our canonicalizer.
1051  if (getenv("CCT_SHAPE_DEBUG")) {
1052  for (THREADID t : g_threads) {
1053  auto* td = GetTls(t);
1054  if (!td)
1055  continue;
1056  fprintf(stderr, "== thread %u has %zu handles\n", t, td->hits.size());
1057  int i = 0;
1058  for (const auto& kv : td->hits) {
1059  if (i++ >= 5)
1060  break;
1061  fprintf(stderr, "-- handle %u --\n", kv.first);
1063  fprintf(stderr, "\n");
1064  }
1065  }
1066  }
1067 
1068  CctInventory inv;
1069  for (THREADID t : g_threads)
1070  BuildInventoryFromThread(t, inv);
1071 
1073  r.checkName = check.c_str();
1074  it->second(inv, r);
1075 
1076  if (!r.failures.empty()) {
1077  fprintf(stderr, "cct_shape_check[%s] %zu failure(s):\n",
1078  check.c_str(), r.failures.size());
1079  for (const auto& f : r.failures)
1080  fprintf(stderr, " %s\n", f.c_str());
1081  }
1082 
1083  // Optional: dump inventory summary even on success (for probing
1084  // fresh victims / setting assertion thresholds).
1085  if (!r.failures.empty() || getenv("CCT_SHAPE_ALWAYS_DUMP")) {
1086  fprintf(stderr, "-- inventory summary [%s] --\n", check.c_str());
1087  fprintf(stderr, "distinct_chains=%zu total_distinct_handles=%zu max_depth=%zu\n",
1088  inv.chains.size(), inv.totalDistinctHandles, inv.maxDepthObserved);
1089  for (const auto& kv : inv.byLeafFn) {
1090  fprintf(stderr, " leaf_fn '%s' chains=%zu hits=%llu\n",
1091  kv.first.c_str(), kv.second.size(),
1092  (unsigned long long)inv.hitsByLeafFn[kv.first]);
1093  }
1094  // If CCT_SHAPE_DUMP_CHAINS is set, dump every chain (root->leaf)
1095  // as "; "-separated for eyeballing structural anchoring.
1096  if (getenv("CCT_SHAPE_DUMP_CHAINS")) {
1097  for (const auto& kv : inv.chains) {
1098  fprintf(stderr, " CHAIN hits=%llu :", (unsigned long long)kv.second);
1099  for (const auto& n : kv.first)
1100  fprintf(stderr, " %s ;", n.c_str());
1101  fprintf(stderr, "\n");
1102  }
1103  }
1104  }
1105 
1106  if (!r.failures.empty())
1107  PIN_ExitProcess(1);
1108 }
1109 
1110 // FiniFunc is a fallback if for some reason ThreadFini didn't fire
1111 // (e.g. abnormal exit path). Same body; the `done` flag in ThreadFini
1112 // prevents double-run when both fire.
1113 static void FiniFunc(INT32 code, VOID* v) {
1114  if (g_checkRan)
1115  return;
1116  g_checkRan = true;
1117  // Fallback: ThreadFini didn't fire (abnormal exit path). At this
1118  // point images may already be unloaded and cctlib's IsValidIP
1119  // will return false for every frame -> chains come back as
1120  // BAD IP !!. The assertions catch that as a sentinel failure and
1121  // the diagnostic makes the cause obvious.
1122  RunChecksAndExit(KnobCheck.Value());
1123 }
1124 
1125 // ---------------- main --------------------------------------------
1126 
1127 static INT32 Usage() {
1128  PIN_ERROR("cct_shape_check: cctlib CCT-shape assertion tool. Requires -check <victim_name>.\n" + KNOB_BASE::StringKnobSummary() + "\n");
1129  return -1;
1130 }
1131 
1132 static FILE* gTraceFile;
1133 
1134 int main(int argc, char* argv[]) {
1135  if (PIN_Init(argc, argv))
1136  return Usage();
1137  PIN_InitSymbols();
1138 
1139  // cctlib requires a log file; keep it silent by opening /dev/null.
1140  gTraceFile = fopen("/dev/null", "w");
1141  if (!gTraceFile)
1142  gTraceFile = stderr;
1143 
1144  PIN_InitLock(&g_lock);
1145  g_tlsKey = PIN_CreateThreadDataKey(nullptr);
1146  PIN_AddThreadStartFunction(ThreadStartRegister, nullptr);
1147  PIN_AddThreadFiniFunction(ThreadFini, nullptr);
1148  PIN_AddFiniFunction(FiniFunc, nullptr);
1149 
1150  // Call/ret-only instrumentation. cctlib always slots every CALL
1151  // and RET; our InterestingInsCallOrRet just tells cctlib to
1152  // additionally invoke our InstrumentInsCallback at those slots.
1153  // Every reached TraceNode contains at least one call or ret
1154  // (Pin traces almost always end on a control-flow insn), so this
1155  // is enough to enumerate the full CCT via GetFullCallingContext
1156  // from any recorded handle -- and it skips all the memory-access
1157  // instrumentation that was dominating runtime.
1159  // Hook main's entry AFTER PinCCTLibInit so cctlib's
1160  // TRACE_AddInstrumentFunction is registered before our
1161  // RTN_InsertCall (order of instrumentation callbacks in Pin does
1162  // NOT depend on registration order but analysis-routine order at
1163  // the same IPOINT does -- see CALL_ORDER_LAST in CaptureMainHandle
1164  // above).
1165  IMG_AddInstrumentFunction(OnImgLoad, nullptr);
1166 
1167  PIN_StartProgram();
1168  return 0;
1169 }
static void check_exc_deep_unwind(const CctInventory &inv, AssertionRecorder &r)
int main(int argc, char *argv[])
static void check_exc_none_tn(const CctInventory &inv, AssertionRecorder &r)
static void check_exc_ctor_throw(const CctInventory &inv, AssertionRecorder &r)
static bool isSentinelName(const string &n)
static FILE * gTraceFile
static vector< THREADID > g_threads
static KNOB< string > KnobCheck(KNOB_MODE_WRITEONCE, "pintool", "check", "", "name of the per-victim CCT-shape check function to run at Fini (required)")
static void FiniFunc(INT32 code, VOID *v)
static void check_exc_dtor_cleanup(const CctInventory &inv, AssertionRecorder &r)
static bool g_checkRan
static void RunChecksAndExit(const string &check)
static void check_rec_fib_deep(const CctInventory &inv, AssertionRecorder &r)
static VOID OnImgLoad(IMG img, VOID *)
static void check_exc_stress_loop(const CctInventory &inv, AssertionRecorder &r)
static INT32 Usage()
static VOID CaptureMainHandle(uint32_t opaqueHandle, THREADID t)
static bool isRootName(const string &n)
static void check_rec_baseline_nonrec(const CctInventory &inv, AssertionRecorder &r)
static PIN_LOCK g_lock
static void check_exc_catch_and_resume(const CctInventory &inv, AssertionRecorder &r)
static void check_sig_sigsegv_recover(const CctInventory &inv, AssertionRecorder &r)
static const set< string > kThrowMachineryFns
static void check_sig_longjmp(const CctInventory &inv, AssertionRecorder &r)
static void check_rec_ackermann(const CctInventory &inv, AssertionRecorder &r)
static VOID InstrumentInsCallback(INS ins, VOID *, const uint32_t slot)
static void check_exc_uncaught_tn(const CctInventory &inv, AssertionRecorder &r)
BOOL InterestingInsCallOrRet(INS ins)
static void check_exc_polymorphic(const CctInventory &inv, AssertionRecorder &r)
static VOID ThreadFini(THREADID t, const CONTEXT *, INT32, VOID *)
static VOID RecordCtxt(uint32_t opaqueHandle, THREADID t)
void(*)(const CctInventory &, AssertionRecorder &) CheckFn
static void expectMarkerAnchored(AssertionRecorder &r, const CctInventory &inv, const string &fn, const string &parent)
static bool isSoftSentinelName(const string &n)
static void check_rec_exception(const CctInventory &inv, AssertionRecorder &r)
static void check_exc_simple_throw(const CctInventory &inv, AssertionRecorder &r)
static bool isHardSentinelName(const string &n)
static TLS_KEY g_tlsKey
static void check_exc_recurse_trycatch(const CctInventory &inv, AssertionRecorder &r)
static void check_rec_indirect_only(const CctInventory &inv, AssertionRecorder &r)
static void check_rec_multi_direct(const CctInventory &inv, AssertionRecorder &r)
vector< string > FnChain
static VOID ThreadStart(THREADID t, CONTEXT *, INT32, VOID *)
static ADDRINT g_mainLo
static const map< string, CheckFn > kChecks
static VOID ThreadStartRegister(THREADID t, CONTEXT *ctxt, INT32 flags, VOID *v)
static void check_exc_rethrow(const CctInventory &inv, AssertionRecorder &r)
static ADDRINT g_mainHi
static TData * GetTls(THREADID t)
static void check_rec_stripped(const CctInventory &inv, AssertionRecorder &r)
static void check_exc_catchall(const CctInventory &inv, AssertionRecorder &r)
static void BuildInventoryFromThread(THREADID t, CctInventory &inv)
static void check_rec_mixed_direct_indirect(const CctInventory &inv, AssertionRecorder &r)
int C[N]
Definition: test3.c:5
int PinCCTLibInit(IsInterestingInsFptr isInterestingIns, FILE *logFile, CCTLibInstrumentInsCallback userCallback, VOID *userCallbackArg, BOOL doDataCentric=false)
Definition: cctlib.cpp:3132
hpcrun_metricFlags_t flags
Definition: cctlib.cpp:3425
VOID PrintFullCallingContext(const ContextHandle_t ctxtHandle)
Definition: cctlib.cpp:2346
VOID GetFullCallingContext(const ContextHandle_t curCtxtHndle, std::vector< Context > &contextVec)
Definition: cctlib.cpp:2339
ContextHandle_t GetContextHandle(const THREADID id, const uint32_t slot)
Definition: cctlib.cpp:1356
uint32_t ContextHandle_t
Definition: cctlib.H:22
ContextHandle_t GetTraceStartHandle(ContextHandle_t ctxtHandle)
Definition: cctlib.cpp:3249
#define PIN_ExitProcess(a)
Definition: shadow_memory.H:35
void expectGE(A obs, B lim, const char *what)
void expectEQ(A obs, B want, const char *what)
void expectNoSentinels(const CctInventory &inv)
void expectInRange(A obs, B lo, C hi, const char *what)
void expectTrue(bool b, const char *what)
vector< string > failures
const char * checkName
void expectLE(A obs, B lim, const char *what)
size_t subMaxCountInAnyChain(const string &fn) const
bool everyChainToFnHasImmediateParent(const string &leafFn, const string &expectedParent) const
bool noAncestorOfFnIsInSet(const string &fn, const set< string > &badAncestors) const
size_t subMaxDepthObserved
bool subAnyChainContainsFn(const string &fn) const
map< string, size_t > sentinelCounts
bool hasFn(const string &fn) const
size_t totalDistinctHandles
map< FnChain, uint64_t > subChains
map< string, size_t > softSentinelCounts
map< string, set< FnChain > > byLeafFn
bool hasChain(const vector< string > &expected) const
map< string, uint64_t > hitsByLeafFn
size_t maxCountInAnyChain(const string &fn) const
bool anyChainContainsFn(const string &fn) const
size_t chainCountForFn(const string &fn) const
map< string, set< FnChain > > subByLeafFn
bool everySubChainToFnHasImmediateParent(const string &leafFn, const string &expectedParent) const
size_t subChainCountForFn(const string &fn) const
size_t subChainsContainingFn(const string &fn) const
map< FnChain, uint64_t > chains
bool subHasFn(const string &fn) const
unordered_map< uint32_t, uint64_t > hits
ContextHandle_t mainCtxtHndl
void h()
Definition: test1.c:12
void f()
Definition: test1.c:23