% sanitizers-runtime-checkers.tex — ASan (shadow memory, redzones, quarantine),
% TSan (happens-before, vector clocks), UBSan (C languages only), Main Thread
% Checker, Thread Performance Checker, Malloc Scribble / Guard Edges / Guard
% Malloc, Zombies (template removed in Xcode 26), Swift exclusivity enforcement;
% how to read ASan and TSan reports; which tools combine; CI flags.
% Sources (checked 2026-09-25): developer.apple.com "Diagnosing memory, thread, and
% crash issues early" (what each detects, languages, overhead numbers, TSan not on
% iOS devices, MTC defaults); "Diagnosing performance issues early" (Thread
% Performance Checker, Runtime API Checking in test plans); clang.llvm.org
% AddressSanitizer / ThreadSanitizer docs; Apple "Enabling the Malloc Debugging
% Features" + malloc env-var notes (0x55 / 0xAA, guard pages >= 4096 B, Guard
% Malloc Mac + Simulator only); Xcode 26 release notes (Zombies template removed,
% ASan container-overflow default); Xcode 27 release notes (ASan launch issue);
% swift.org "Swift 5 Exclusivity Enforcement". ASan+UBSan and TSan+UBSan both
% seen enabled together (Apple forum screenshot, xcodebuild CI example) — secondary.
% Not repeated: exception types / crash reports (security-build/crashes-symbolication),
% the C UB catalogue (c/c-undefined-behavior-and-build), Instruments (ios-swift/instruments-performance).
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/debugging/sanitizers-runtime-checkers.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=debugging kind=tooling level=senior platform=apple new=no round=market-2026-09-25 topic=debugging,memory,concurrency
% @tags: asan, tsan, ubsan, shadow-memory, redzone, happens-before, data-race, main-thread-checker, thread-performance-checker, malloc-scribble, nszombie, exclusivity-enforcement
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{SwiftSheet}{
  morekeywords={final,class,var,let,func,inout,return,self},
  sensitive=true, morecomment=[l]{//}, morestring=[b]",
  literate={==}{{\hbox{=}\hbox{=}}}2 {->}{{\hbox{-}\hbox{>}}}2 {+=}{{\hbox{+}\hbox{=}}}2}
\lstdefinelanguage{ReportSheet}{
  sensitive=true, morekeywords={ERROR,WARNING,SUMMARY,READ,Write},
  morecomment=[l]{//},
  literate={==}{{\hbox{=}\hbox{=}}}2 {--}{{\hbox{-}\hbox{-}}}2 {->}{{\hbox{-}\hbox{>}}}2}
\lstset{basicstyle=\ttfamily\footnotesize, aboveskip=2pt, belowskip=2pt}

\newcommand\ct[1]{\texttt{#1}}

\tikzset{
  mem/.style={draw=sheetGrey, minimum height=5mm, font=\ttfamily\tiny, inner sep=0.5pt},
  sh/.style={draw=sheetGrey, minimum height=4mm, font=\ttfamily\tiny, inner sep=0.5pt},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
  hd/.style={font=\bfseries\small, text=sheetBlue, anchor=west},
  ev/.style={circle, fill=#1, inner sep=0pt, minimum size=1.8mm},
}

\begin{document}

\sheettitle{Sanitizers \& runtime checkers — make the bug crash \emph{here}}{debugging · memo}

\oneliner{Memory and threading bugs corrupt silently and crash \emph{later,
elsewhere}. Sanitizers are \textbf{compiler instrumentation + a runtime} that
check every access as it happens (ASan: is this byte addressable? TSan: is there
a happens-before edge?) and stop at the \textbf{first bad access} with the
stacks that explain it. Runtime checkers (Main Thread Checker, Thread Performance
Checker, exclusivity) need no special build. Price: slowdown — so they live in
Debug schemes, test plans and CI, never in the shipped app.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  % ================= ASan
  \node[hd] at (-0.1,2.75) {ASan: shadow memory + redzones + quarantine};
  \node[lbl, anchor=west, align=left] at (-0.1,1.72) {app\\heap};
  \node[mem, minimum width=10mm, fill=sheetRed!18] (rz1) at (1.35,1.72) {redzone};
  \node[mem, minimum width=22mm, fill=sheetGreen!15] (blk) at (2.95,1.72) {\ct{malloc(16)} block};
  \node[mem, minimum width=10mm, fill=sheetRed!18] (rz2) at (4.55,1.72) {redzone};
  \node[mem, minimum width=13mm, fill=sheetGrey!25] (fr) at (6.3,1.72) {freed};
  \node[lbl, anchor=west, align=left] at (-0.1,1.05) {shadow\\1 B / 8 B};
  \node[sh, minimum width=10mm, fill=sheetRed!18] at (1.35,1.05) {fa fa};
  \node[sh, minimum width=22mm, fill=sheetGreen!15] at (2.95,1.05) {00 00};
  \node[sh, minimum width=10mm, fill=sheetRed!18] at (4.55,1.05) {fa fa};
  \node[sh, minimum width=13mm, fill=sheetGrey!25] at (6.3,1.05) {fd fd};
  \foreach \x in {1.35,2.95,4.55,6.3} \draw[sheetGrey!60, densely dotted] (\x,1.47) -- (\x,1.25);
  \node[lbl, anchor=south, text=sheetGrey] at (6.3,1.97) {quarantine};
  \draw[hot, sheetRed] (4.2,2.3) node[above, lbl, text=sheetRed]{\ct{p[16]}: overflow} -- (rz2.north);
  \draw[hot, sheetRed] (5.4,2.3) node[above, lbl, text=sheetRed]{stale \ct{p}} -- (fr.north west);
  \node[lbl, anchor=north west, align=left, text width=76mm] at (-0.1,0.78) {every load/store first checks its shadow byte:
  \ct{00} = addressable, \ct{fa} = heap redzone, \ct{fd} = freed. Freed blocks wait in \textbf{quarantine}
  (not reused yet), so a stale pointer hits poisoned memory $\to$ report + stop.};
  \draw[sheetGrey!50] (7.95,2.9) -- (7.95,-0.1);
  % ================= TSan
  \node[hd] at (8.05,2.75) {TSan: is there a happens-before edge?};
  \node[lbl, anchor=east] at (9.0,1.75) {main};
  \node[lbl, anchor=east] at (9.0,1.05) {T2};
  \draw[flow] (9.05,1.75) -- (12.3,1.75); \draw[flow] (9.05,1.05) -- (12.3,1.05);
  \node[ev=sheetBlue, label={[lbl]above:read \ct{x}}] (r1) at (9.55,1.75) {};
  \node[ev=sheetOrange, label={[lbl]below:write \ct{x}}] (w1) at (10.65,1.05) {};
  \draw[<->, thick, sheetRed, dashed] (r1) -- node[lbl, right, text=sheetRed, pos=0.55]{no edge: \textbf{race}} (w1);
  \node[lbl, anchor=east] at (13.2,1.75) {main};
  \node[lbl, anchor=east] at (13.2,1.05) {T2};
  \draw[flow] (13.25,1.75) -- (16.55,1.75); \draw[flow] (13.25,1.05) -- (16.55,1.05);
  \node[ev=sheetBlue, label={[lbl]above:read}] at (13.55,1.75) {};
  \node[ev=sheetGreen!60!black, label={[lbl]above:unlock}] (u) at (14.3,1.75) {};
  \node[ev=sheetGreen!60!black, label={[lbl]below:lock}] (l) at (15.2,1.05) {};
  \node[ev=sheetOrange, label={[lbl]below:write}] at (16.0,1.05) {};
  \draw[->, thick, sheetGreen!60!black] (u) -- node[lbl, right, pos=0.25, text=sheetGreen!45!black]{edge: ok} (l);
  \node[lbl, anchor=north west, align=left, text width=84mm] at (8.05,0.78) {each thread carries a \textbf{vector clock};
  locks, queues, atomics, task hand-offs create edges. Two accesses to one address, $\ge$1 write, no edge
  $\to$ race — even if this run \emph{happened} to be fine.};
\end{tikzpicture}

\begin{multicols}{2}
\small\setstretch{1.0}\raggedright

\section{Tool $\to$ bug class $\to$ cost $\to$ combine?}
{\footnotesize\setlength{\tabcolsep}{2.5pt}
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{12mm}>{\raggedright\arraybackslash}p{30mm}>{\raggedright\arraybackslash}p{14mm}>{\raggedright\arraybackslash}p{18mm}@{}}
\toprule
\textbf{tool} & \textbf{catches} & \textbf{cost} & \textbf{combine}\\ \midrule
\textbf{ASan} & overflow (heap/stack/global), use-after-free/return/scope, double free. C + Swift & 2–5$\times$ CPU, 2–3$\times$ mem & \textbf{not TSan}; + UBSan; device ok\\
\textbf{TSan} & data races, Swift access races, bad mutexes, thread leaks. C + Swift & 2–20$\times$ CPU, 5–10$\times$ mem & \textbf{not ASan}; \textbf{Simulator/ macOS only}\\
\textbf{UBSan} & C UB: overflow, shifts, null, misaligned, bad enum/bool. \textbf{Not Swift} & $\approx$20\,\% & + ASan or TSan\unverified\\
\textbf{MTC} & UIKit/AppKit API off main & 1–2\,\%, $\le$100\,ms launch & default on, no rebuild\\
\textbf{TPC} & priority inversion; sync I/O, networking on main & low & on for Run, no rebuild\\
Malloc Scribble & free $\to$ \ct{0x55}, alloc $\to$ \ct{0xAA} & cheap & checkbox\\
Guard Edges & guard pages round $\ge$4\,KB blocks & memory & checkbox\\
Guard Malloc & page per alloc: fault at once & huge & Mac + Sim only\\
Zombies & message to a freed ObjC object & never frees & scheme / Allocations\\
Exclusivity & overlapping \ct{inout}/\ct{mutating} (Swift) & small & always, Release too\\
\bottomrule
\end{tabular}}
{\footnotesize MTC = Main Thread Checker · TPC = Thread Performance Checker.}

\section{How they work}
\begin{itemize}
  \item \textbf{ASan}: the compiler inserts a shadow check before every
        load/store; the allocator adds poisoned \emph{redzones} around each
        block and \emph{quarantines} freed memory. Apple: does \emph{not} find
        leaks, uninitialised reads or integer overflow.
  \item \textbf{TSan}: instruments accesses \emph{and} synchronisation, keeps
        the last few accesses per 8 bytes in shadow cells, compares vector
        clocks\unverified. Only paths you \emph{executed} are checked — drive concurrency
        in tests. Swift 6 checks isolation at compile time; TSan still owns
        locks, \ct{@unchecked Sendable}, C/ObjC and \ct{unsafe} code.
  \item \textbf{Main Thread Checker} / \textbf{Thread Performance Checker}:
        runtime interposition, no recompile, show as purple runtime issues;
        can \emph{pause on issue}. TPC flags a user-interactive thread waiting
        on lower-QoS work (e.g.\ a semaphore — no priority donation).
  \item \textbf{Zombies}: Xcode 26 removed the Zombies \emph{template} — use
        Allocations + \emph{Enable NSZombie detection}, or the scheme's
        \emph{Zombie Objects}. Freed objects become zombies that log
        ``message sent to deallocated instance''.
  \item \textbf{Exclusivity}: statically where it can, dynamically for class
        properties, globals, escaping captures; traps with ``Simultaneous
        accesses to \ldots, but modification requires exclusive access''.
\end{itemize}

\section{Turning them on}
Scheme $\to$ Run/Test $\to$ \textbf{Diagnostics}; test plans: one configuration
per sanitizer, \emph{Runtime API Checking} ``On (as Failure)''. CI:
\ct{xcodebuild test -enableAddressSanitizer YES}, then a second run with
\ct{-enableThreadSanitizer YES}; \ct{-enableUndefinedBehaviorSanitizer YES};
\ct{swiftc -sanitize=address}. Tune with \ct{ASAN\_OPTIONS} (Xcode 26: C++
container-overflow checks on by default — \ct{detect\_container\_overflow=0} to
silence).

\columnbreak

\section{Reading an ASan report {\normalfont\scriptsize(abridged)}}
\begin{lstlisting}[language=ReportSheet]
==4242==ERROR: AddressSanitizer: heap-use-after-free
  on address 0x60200000eff0 ...
READ of size 8 at 0x60200000eff0 thread T0
  #0 parse_header  buf.c:42        // 1 WHERE: bad access
0x60200000eff0 is located 0 bytes inside of 16-byte region
freed by thread T0 here:
  #1 conn_close    conn.c:88       // 2 WHO freed it
previously allocated by thread T0 here:
  #1 conn_open     conn.c:31       // 3 WHO made it
SUMMARY: AddressSanitizer: heap-use-after-free buf.c:42
\end{lstlisting}
Read the \textbf{bug type} (heap-buffer-overflow, stack-use-after-return…),
then three stacks: the access, the free, the allocation. The fix is almost
always in \#2 or \#3, not \#1.

\section{Reading a TSan report {\normalfont\scriptsize(abridged)}}
\begin{lstlisting}[language=ReportSheet]
WARNING: ThreadSanitizer: data race (pid=4242)
  Write of size 8 at 0x7b0800001234 by thread T3:
    #0 Cache.store(_:)   Cache.swift:18
  Previous read of size 8 at 0x7b0800001234 by main thread:
    #0 Cache.lookup(_:)  Cache.swift:11
  Location is heap block of size 48 ... allocated by main thread
  Thread T3 (running) created by main thread at: ...
SUMMARY: ThreadSanitizer: data race Cache.swift:18
\end{lstlisting}
Two stacks, two threads, one address, no edge. Fix the \emph{ownership}
(actor, serial queue, \ct{Mutex}), not the timing.

\section{Example — exclusivity trap at run time}
\begin{lstlisting}[language=SwiftSheet]
final class Counter { var n = 0 }
func bump(_ x: inout Int, by c: Counter) { x += c.n }
let c = Counter()
bump(&c.n, by: c)   // write access to c.n open, then read c.n
// traps: "Simultaneous accesses to ..." (see above)
\end{lstlisting}

\section{Interview traps}
\begin{itemize}
  \trap{``Run ASan and TSan together'' — impossible; two test-plan configs.}
  \trap{TSan on a physical iPhone — not supported; use the Simulator.}
  \trap{UBSan for Swift — Swift traps on overflow/bounds already; UBSan is C-only.}
  \trap{Clean TSan run $\neq$ race-free: only executed interleavings count.}
\end{itemize}

\section{Remember}
\textbf{ASan = where memory, TSan = when threads, UBSan = C rules, MTC/TPC =
the main thread.} Crash here, not later.

\section{Likely questions}
\begin{enumerate}
  \item How does ASan find an overflow? — shadow byte check hits a redzone.
  \item Why not ship with ASan? — 2–5$\times$ slower, 2–3$\times$ memory.
  \item \ct{EXC\_BAD\_ACCESS} in ObjC? — Zombies, then ASan.
  \item Priority inversion? — TPC; replace the semaphore wait with async/await.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} crashes-symbolication
(exception types) · c-undefined-behavior-and-build (UB catalogue) ·
instruments-performance (Allocations, Hangs) · lldb-deep · Swift concurrency (actors, \ct{Sendable})}

\end{document}
