zeroize-audit — Claude Skill When to Use - Auditing cryptographic implementations (keys, seeds, nonces, secrets) - Reviewing authentication systems (passwords, tokens, session data) - Analyzing code that handles PII or sensitive credentials - Verifying secure cleanup in security-critical codebases - Investigating memory safety of sensitive data handling When NOT to Use - General code review without security focus - Performance optimization (unless related to secure wiping) - Refactoring tasks not related to sensitive data - Code without identifiable secrets or sensitive values --- Purpose Det…

\\n'/\\\\n}\"\n s=\"${s//

zeroize-audit — Claude Skill When to Use - Auditing cryptographic implementations (keys, seeds, nonces, secrets) - Reviewing authentication systems (passwords, tokens, session data) - Analyzing code that handles PII or sensitive credentials - Verifying secure cleanup in security-critical codebases - Investigating memory safety of sensitive data handling When NOT to Use - General code review without security focus - Performance optimization (unless related to secure wiping) - Refactoring tasks not related to sensitive data - Code without identifiable secrets or sensitive values --- Purpose Det…

\\t'/\\\\t}\"\n printf '%s' \"$s\"\n}\n\nASM=\"\"\nSYMBOL=\"\"\nOUT=\"\"\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --asm)\n ASM=\"$2\"\n shift 2\n ;;\n --symbol)\n SYMBOL=\"$2\"\n shift 2\n ;;\n --out)\n OUT=\"$2\"\n shift 2\n ;;\n *)\n echo \"Unknown arg: $1\" >&2\n usage\n exit 2\n ;;\n esac\ndone\n\nif [[ -z \"$ASM\" || -z \"$OUT\" ]]; then\n usage\n exit 2\nfi\n\nif [[ ! -f \"$ASM\" ]]; then\n echo \"Assembly file not found: $ASM\" >&2\n exit 2\nfi\n\n# Extract function boundaries if symbol specified\nSTART_LINE=1\nEND_LINE=$(wc -l \u003c\"$ASM\")\n\nif [[ -n \"$SYMBOL\" ]]; then\n # Find function start/end\n START_LINE=$(grep -n \"^${SYMBOL}:\" \"$ASM\" | head -1 | cut -d: -f1 || echo \"\")\n if [[ -z \"$START_LINE\" ]]; then\n echo \"WARNING: symbol '${SYMBOL}' not found in $ASM; analyzing full file\" >&2\n START_LINE=1\n fi\n # Find next function or end of file\n END_LINE=$(tail -n +\"$((START_LINE + 1))\" \"$ASM\" | grep -n \"^[a-zA-Z_][a-zA-Z0-9_]*:\" | head -1 | cut -d: -f1 || echo \"$(($(wc -l \u003c\"$ASM\") - START_LINE + 1))\")\n END_LINE=$((START_LINE + END_LINE - 1))\nfi\n\n# Extract function body\nFUNC_ASM=$(sed -n \"${START_LINE},${END_LINE}p\" \"$ASM\")\n\n# Detect patterns\nREGISTER_SPILLS=()\nSTACK_STORES=()\nCALLEE_SAVED_PUSHES=()\nSTACK_SIZE=0\nRED_ZONE_CLEARED=false\n\n# Parse assembly\nwhile IFS= read -r line; do\n # Skip comments and empty lines\n [[ \"$line\" =~ ^[[:space:]]*# ]] && continue\n [[ -z \"${line// /}\" ]] && continue\n\n # Detect stack allocation (subq $size, %rsp)\n if [[ \"$line\" =~ subq[[:space:]]+\\$([0-9]+),[[:space:]]*%rsp ]]; then\n STACK_SIZE=\"${BASH_REMATCH[1]}\"\n fi\n\n # Detect register spills to stack (movq/movdqa/movaps %reg, -offset(%rsp/%rbp))\n if [[ \"$line\" =~ (movq|movdqa|movaps|movups|vmovdqa|vmovaps)[[:space:]]+%([a-z0-9]+),[[:space:]]*-([0-9]+)\\(%(rsp|rbp)\\) ]]; then\n REG=\"${BASH_REMATCH[2]}\"\n OFFSET=\"${BASH_REMATCH[3]}\"\n BASE=\"${BASH_REMATCH[4]}\"\n REGISTER_SPILLS+=(\"{\\\"register\\\": \\\"$REG\\\", \\\"offset\\\": -$OFFSET, \\\"base\\\": \\\"$BASE\\\", \\\"line\\\": \\\"$(json_escape \"$line\")\\\"}\")\n fi\n\n # Detect stores to stack (mov* reg/imm, -offset(%rsp/%rbp))\n if [[ \"$line\" =~ mov[a-z]*[[:space:]]+[^,]+,[[:space:]]*-([0-9]+)\\(%(rsp|rbp)\\) ]]; then\n OFFSET=\"${BASH_REMATCH[1]}\"\n BASE=\"${BASH_REMATCH[2]}\"\n STACK_STORES+=(\"{\\\"offset\\\": -$OFFSET, \\\"base\\\": \\\"$BASE\\\", \\\"line\\\": \\\"$(json_escape \"$line\")\\\"}\")\n fi\n\n # Detect callee-saved register pushes (pushq %rbx/%r12/%r13/%r14/%r15/%rbp)\n if [[ \"$line\" =~ pushq[[:space:]]+%(rbx|r12|r13|r14|r15|rbp) ]]; then\n REG=\"${BASH_REMATCH[1]}\"\n CALLEE_SAVED_PUSHES+=(\"{\\\"register\\\": \\\"$REG\\\", \\\"line\\\": \\\"$(json_escape \"$line\")\\\"}\")\n fi\n\n # Detect red-zone clearing (movq $0, -offset(%rsp) for offset \u003c= 128)\n if [[ \"$line\" =~ movq[[:space:]]+\\$0,[[:space:]]*-([0-9]+)\\(%rsp\\) ]]; then\n OFFSET=\"${BASH_REMATCH[1]}\"\n if [[ \"$OFFSET\" -le 128 ]]; then\n RED_ZONE_CLEARED=true\n fi\n fi\n\ndone \u003c\u003c\u003c\"$FUNC_ASM\"\n\n# Generate JSON report\nmkdir -p \"$(dirname \"$OUT\")\"\n\ncat >\"$OUT\" \u003c\u003cEOF\n{\n \"asm_file\": \"$ASM\",\n \"symbol\": \"$SYMBOL\",\n \"analysis\": {\n \"stack_size\": $STACK_SIZE,\n \"red_zone_cleared\": $RED_ZONE_CLEARED,\n \"register_spills\": [\n $(\n IFS=,\n echo \"${REGISTER_SPILLS[*]}\"\n)\n ],\n \"stack_stores\": [\n $(\n IFS=,\n echo \"${STACK_STORES[*]}\"\n)\n ],\n \"callee_saved_pushes\": [\n $(\n IFS=,\n echo \"${CALLEE_SAVED_PUSHES[*]}\"\n)\n ]\n },\n \"warnings\": []\n}\nEOF\n\n# Validate JSON output\nif command -v jq &>/dev/null; then\n if ! jq empty \"$OUT\" 2>/dev/null; then\n echo \"ERROR: generated JSON is malformed: $OUT\" >&2\n exit 1\n fi\nfi\n\n# Add warnings based on findings\nWARNINGS=()\n\nif [[ ${#REGISTER_SPILLS[@]} -gt 0 ]]; then\n WARNINGS+=(\"{\\\"type\\\": \\\"REGISTER_SPILL\\\", \\\"message\\\": \\\"Found ${#REGISTER_SPILLS[@]} register spill(s) to stack. Spilled values may contain secrets.\\\"}\")\nfi\n\nif [[ $STACK_SIZE -gt 0 ]] && [[ \"$RED_ZONE_CLEARED\" == \"false\" ]]; then\n WARNINGS+=(\"{\\\"type\\\": \\\"STACK_RETENTION\\\", \\\"message\\\": \\\"Stack frame (${STACK_SIZE} bytes) may retain secrets after function return. Consider clearing red-zone.\\\"}\")\nfi\n\nif [[ ${#CALLEE_SAVED_PUSHES[@]} -gt 0 ]]; then\n WARNINGS+=(\"{\\\"type\\\": \\\"CALLEE_SAVED_SPILL\\\", \\\"message\\\": \\\"Callee-saved registers pushed to stack. If they contain secrets, stack will retain them.\\\"}\")\nfi\n\n# Update JSON with warnings\nif [[ ${#WARNINGS[@]} -gt 0 ]]; then\n WARNINGS_JSON=$(\n IFS=,\n echo \"${WARNINGS[*]}\"\n )\n if command -v jq &>/dev/null; then\n TMP=$(mktemp)\n jq \".warnings = [$WARNINGS_JSON]\" \"$OUT\" >\"$TMP\" && mv \"$TMP\" \"$OUT\"\n else\n echo \"WARNING: jq not found; warnings could not be added to output\" >&2\n fi\nfi\n\necho \"OK: assembly analysis written to $OUT\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5286,"content_sha256":"f072d58e5874b28c9b53ed26a2c7118a3cd994cb75504d56c4dc83ff5fd9de43"},{"filename":"tools/analyze_cfg.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\nControl-Flow Graph analyzer for zeroization path coverage.\n\nThis tool builds CFGs from source code or LLVM IR to verify that:\n- Zeroization occurs on ALL execution paths\n- Early returns don't skip cleanup\n- Error paths include proper cleanup\n- Wipes dominate all function exits\n\"\"\"\n\nimport argparse\nimport json\nimport re\nimport sys\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\n\n\n@dataclass\nclass CFGNode:\n \"\"\"Node in control flow graph.\"\"\"\n\n id: str\n type: str # 'entry', 'exit', 'statement', 'branch', 'return'\n line_num: int | None = None\n statement: str | None = None\n successors: list[str] = field(default_factory=list)\n predecessors: list[str] = field(default_factory=list)\n has_wipe: bool = False\n has_sensitive_var: bool = False\n\n\nclass CFGBuilder:\n \"\"\"Build control flow graph from source or IR.\"\"\"\n\n def __init__(self, source_file: Path, sensitive_patterns: list[str], wipe_patterns: list[str]):\n self.source_file = source_file\n self.sensitive_patterns = sensitive_patterns\n self.wipe_patterns = wipe_patterns\n self.nodes: dict[str, CFGNode] = {}\n self.entry_node: str | None = None\n self.exit_nodes: set[str] = set()\n self.node_counter = 0\n\n def create_node(\n self, node_type: str, line_num: int | None = None, statement: str | None = None\n ) -> str:\n \"\"\"Create a new CFG node.\"\"\"\n node_id = f\"node_{self.node_counter}\"\n self.node_counter += 1\n\n node = CFGNode(id=node_id, type=node_type, line_num=line_num, statement=statement)\n\n # Check if this node has sensitive variable\n if statement:\n for pattern in self.sensitive_patterns:\n if re.search(pattern, statement, re.IGNORECASE):\n node.has_sensitive_var = True\n break\n\n # Check if this node has wipe\n for pattern in self.wipe_patterns:\n if re.search(pattern, statement):\n node.has_wipe = True\n break\n\n self.nodes[node_id] = node\n return node_id\n\n def add_edge(self, from_id: str, to_id: str) -> None:\n \"\"\"Add directed edge in CFG.\"\"\"\n if from_id in self.nodes and to_id in self.nodes:\n self.nodes[from_id].successors.append(to_id)\n self.nodes[to_id].predecessors.append(from_id)\n\n def build_from_source(self) -> None:\n \"\"\"Build CFG from source code (simplified C/C++ parser).\"\"\"\n with open(self.source_file) as f:\n lines = f.readlines()\n\n self.entry_node = self.create_node(\"entry\")\n current_node = self.entry_node\n\n in_function = False\n brace_depth = 0\n branch_stack = [] # Stack of (condition_node, merge_node) pairs\n\n for line_num, line in enumerate(lines, 1):\n stripped = line.strip()\n\n # Skip comments and empty lines\n if not stripped or stripped.startswith(\"//\") or stripped.startswith(\"/*\"):\n continue\n\n # Function start\n if \"{\" in line and not in_function:\n in_function = True\n brace_depth = line.count(\"{\")\n continue\n\n if not in_function:\n continue\n\n # Track brace depth\n brace_depth += line.count(\"{\") - line.count(\"}\")\n\n # Function end\n if brace_depth == 0:\n in_function = False\n # Connect to exit\n exit_node = self.create_node(\"exit\", line_num)\n self.add_edge(current_node, exit_node)\n self.exit_nodes.add(exit_node)\n continue\n\n # Return statement\n if re.match(r\"\\s*return\\b\", stripped):\n return_node = self.create_node(\"return\", line_num, stripped)\n self.add_edge(current_node, return_node)\n exit_node = self.create_node(\"exit\", line_num)\n self.add_edge(return_node, exit_node)\n self.exit_nodes.add(exit_node)\n # Reset current for next statement (in case there's dead code)\n current_node = return_node\n continue\n\n # If statement\n if re.match(r\"\\s*if\\s*\\(\", stripped):\n branch_node = self.create_node(\"branch\", line_num, stripped)\n self.add_edge(current_node, branch_node)\n\n # Create merge point for later\n merge_node = self.create_node(\"statement\", line_num, \"// merge point\")\n branch_stack.append((branch_node, merge_node))\n\n # True branch starts after condition\n true_node = self.create_node(\"statement\", line_num, \"// true branch\")\n self.add_edge(branch_node, true_node)\n current_node = true_node\n continue\n\n # Else statement\n if re.match(r\"\\s*else\\b\", stripped):\n if branch_stack:\n branch_node, merge_node = branch_stack[-1]\n # False branch\n false_node = self.create_node(\"statement\", line_num, \"// false branch\")\n self.add_edge(branch_node, false_node)\n # Connect previous path to merge\n self.add_edge(current_node, merge_node)\n current_node = false_node\n continue\n\n # End of branch (closing brace)\n if stripped == \"}\" and branch_stack:\n branch_node, merge_node = branch_stack.pop()\n self.add_edge(current_node, merge_node)\n current_node = merge_node\n continue\n\n # Regular statement\n stmt_node = self.create_node(\"statement\", line_num, stripped)\n self.add_edge(current_node, stmt_node)\n current_node = stmt_node\n\n # Ensure we have at least one exit node\n if not self.exit_nodes:\n exit_node = self.create_node(\"exit\")\n self.add_edge(current_node, exit_node)\n self.exit_nodes.add(exit_node)\n\n def find_all_paths_to_exit(self) -> list[list[str]]:\n \"\"\"Find all paths from entry to any exit node.\"\"\"\n if not self.entry_node:\n return []\n\n all_paths = []\n\n def dfs(node_id: str, path: list[str], visited: set[str]) -> None:\n if node_id in visited:\n return # Avoid cycles\n\n visited.add(node_id)\n path.append(node_id)\n\n node = self.nodes[node_id]\n\n # If this is an exit node, save the path\n if node_id in self.exit_nodes:\n all_paths.append(path.copy())\n else:\n # Continue to successors\n for succ_id in node.successors:\n dfs(succ_id, path, visited.copy())\n\n path.pop()\n\n dfs(self.entry_node, [], set())\n return all_paths\n\n def check_path_has_wipe(self, path: list[str]) -> tuple[bool, str | None]:\n \"\"\"Check if a path contains a wipe operation.\"\"\"\n for node_id in path:\n if self.nodes[node_id].has_wipe:\n return True, node_id\n return False, None\n\n def check_path_has_sensitive_var(self, path: list[str]) -> bool:\n \"\"\"Check if a path uses sensitive variables.\"\"\"\n return any(self.nodes[node_id].has_sensitive_var for node_id in path)\n\n def compute_dominators(self) -> dict[str, set[str]]:\n \"\"\"Compute dominator sets for all nodes.\"\"\"\n if not self.entry_node:\n return {}\n\n # Initialize\n dominators = {}\n all_nodes = set(self.nodes.keys())\n\n dominators[self.entry_node] = {self.entry_node}\n\n for node_id in all_nodes:\n if node_id != self.entry_node:\n dominators[node_id] = all_nodes.copy()\n\n # Iterate until fixpoint\n changed = True\n while changed:\n changed = False\n for node_id in all_nodes:\n if node_id == self.entry_node:\n continue\n\n # Dom(n) = {n} ∪ (∩ Dom(p) for all predecessors p)\n new_dom = {node_id}\n if self.nodes[node_id].predecessors:\n pred_doms = [dominators[pred] for pred in self.nodes[node_id].predecessors]\n if pred_doms:\n new_dom = new_dom.union(set.intersection(*pred_doms))\n\n if new_dom != dominators[node_id]:\n dominators[node_id] = new_dom\n changed = True\n\n return dominators\n\n def verify_wipe_dominates_exits(self) -> dict:\n \"\"\"Verify that wipe operations dominate all exit nodes.\"\"\"\n dominators = self.compute_dominators()\n\n # Find all wipe nodes\n wipe_nodes = [node_id for node_id, node in self.nodes.items() if node.has_wipe]\n\n results = {\n \"wipe_dominates_all_exits\": True,\n \"wipe_nodes\": wipe_nodes,\n \"problematic_exits\": [],\n }\n\n for exit_id in self.exit_nodes:\n exit_doms = dominators.get(exit_id, set())\n\n # Check if any wipe node dominates this exit\n has_dominating_wipe = any(wipe_id in exit_doms for wipe_id in wipe_nodes)\n\n if not has_dominating_wipe:\n results[\"wipe_dominates_all_exits\"] = False\n results[\"problematic_exits\"].append(\n {\n \"exit_node\": exit_id,\n \"line\": self.nodes[exit_id].line_num,\n \"dominators\": list(exit_doms),\n }\n )\n\n return results\n\n def analyze(self) -> dict:\n \"\"\"Perform comprehensive CFG analysis.\"\"\"\n # Find all paths\n all_paths = self.find_all_paths_to_exit()\n\n # Check each path\n paths_with_wipe = 0\n paths_without_wipe = []\n paths_with_sensitive_vars = 0\n\n for i, path in enumerate(all_paths):\n has_wipe, wipe_node = self.check_path_has_wipe(path)\n has_sensitive = self.check_path_has_sensitive_var(path)\n\n if has_wipe:\n paths_with_wipe += 1\n elif has_sensitive:\n # Sensitive path without wipe\n paths_without_wipe.append(\n {\n \"path_id\": i,\n \"length\": len(path),\n \"nodes\": [\n {\n \"id\": node_id,\n \"line\": self.nodes[node_id].line_num,\n \"statement\": self.nodes[node_id].statement,\n }\n for node_id in path\n ],\n }\n )\n\n if has_sensitive:\n paths_with_sensitive_vars += 1\n\n # Dominator analysis\n dominator_results = self.verify_wipe_dominates_exits()\n\n return {\n \"cfg_stats\": {\n \"total_nodes\": len(self.nodes),\n \"total_paths\": len(all_paths),\n \"exit_nodes\": len(self.exit_nodes),\n },\n \"wipe_coverage\": {\n \"paths_with_wipe\": paths_with_wipe,\n \"paths_without_wipe\": len(paths_without_wipe),\n \"paths_with_sensitive_vars\": paths_with_sensitive_vars,\n \"coverage_percentage\": (paths_with_wipe / len(all_paths) * 100) if all_paths else 0,\n },\n \"problematic_paths\": paths_without_wipe,\n \"dominator_analysis\": dominator_results,\n }\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Control-flow graph analyzer\")\n parser.add_argument(\"--src\", required=True, help=\"Source file to analyze\")\n parser.add_argument(\"--out\", required=True, help=\"Output JSON file\")\n\n args = parser.parse_args()\n\n # Default patterns\n sensitive_patterns = [\n r\"\\b(secret|key|seed|priv|private|sk|shared_secret|nonce|token|pwd|pass)\\b\"\n ]\n wipe_patterns = [\n r\"\\bexplicit_bzero\\s*\\(\",\n r\"\\bmemset_s\\s*\\(\",\n r\"\\bOPENSSL_cleanse\\s*\\(\",\n r\"\\bsodium_memzero\\s*\\(\",\n r\"\\bzeroize\\s*\\(\",\n ]\n\n # Build CFG\n builder = CFGBuilder(Path(args.src), sensitive_patterns, wipe_patterns)\n try:\n builder.build_from_source()\n except OSError as e:\n print(f\"Error: cannot read source file {args.src}: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # Analyze\n results = {\"source_file\": args.src, \"analysis\": builder.analyze()}\n\n # Write output\n output_path = Path(args.out)\n output_path.parent.mkdir(parents=True, exist_ok=True)\n\n with open(output_path, \"w\") as f:\n json.dump(results, f, indent=2)\n\n print(f\"OK: CFG analysis written to {args.out}\")\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13056,"content_sha256":"1a5bede45f21383c3377a5e639fa676bfe204154303b80c98cd59feaebec2c32"},{"filename":"tools/analyze_heap.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# Analyze heap allocations for security issues with sensitive data.\n#\n# Usage:\n# analyze_heap.sh --src path/to/file.c --config config.yaml --out /tmp/heap_analysis.json\n#\n# Detects:\n# - malloc/calloc/realloc for sensitive variables (should use secure allocators)\n# - Missing mlock/madvise for sensitive heaps\n# - Secure allocator usage (approved patterns)\n\nusage() {\n echo \"Usage: $0 --src \u003cfile> --out \u003canalysis.json> [--config \u003cconfig.yaml>]\" >&2\n}\n\njson_escape() {\n local s=\"$1\"\n s=\"${s//\\\\/\\\\\\\\}\"\n s=\"${s//\\\"/\\\\\\\"}\"\n s=\"${s//

zeroize-audit — Claude Skill When to Use - Auditing cryptographic implementations (keys, seeds, nonces, secrets) - Reviewing authentication systems (passwords, tokens, session data) - Analyzing code that handles PII or sensitive credentials - Verifying secure cleanup in security-critical codebases - Investigating memory safety of sensitive data handling When NOT to Use - General code review without security focus - Performance optimization (unless related to secure wiping) - Refactoring tasks not related to sensitive data - Code without identifiable secrets or sensitive values --- Purpose Det…

\\n'/\\\\n}\"\n s=\"${s//

zeroize-audit — Claude Skill When to Use - Auditing cryptographic implementations (keys, seeds, nonces, secrets) - Reviewing authentication systems (passwords, tokens, session data) - Analyzing code that handles PII or sensitive credentials - Verifying secure cleanup in security-critical codebases - Investigating memory safety of sensitive data handling When NOT to Use - General code review without security focus - Performance optimization (unless related to secure wiping) - Refactoring tasks not related to sensitive data - Code without identifiable secrets or sensitive values --- Purpose Det…

\\t'/\\\\t}\"\n printf '%s' \"$s\"\n}\n\nSRC=\"\"\nCONFIG=\"\"\nOUT=\"\"\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --src)\n SRC=\"$2\"\n shift 2\n ;;\n --config)\n CONFIG=\"$2\"\n shift 2\n ;;\n --out)\n OUT=\"$2\"\n shift 2\n ;;\n *)\n echo \"Unknown arg: $1\" >&2\n usage\n exit 2\n ;;\n esac\ndone\n\nif [[ -z \"$SRC\" || -z \"$OUT\" ]]; then\n usage\n exit 2\nfi\n\nif [[ ! -f \"$SRC\" ]]; then\n echo \"Source file not found: $SRC\" >&2\n exit 2\nfi\n\n# Load patterns from config\nSENSITIVE_PATTERN=\"(secret|key|seed|priv|private|sk|shared_secret|nonce|token|pwd|pass)\"\nSECURE_ALLOC_FUNCS=\"(OPENSSL_secure_malloc|OPENSSL_secure_zalloc|sodium_malloc|sodium_allocarray|SecureAlloc)\"\n\nif [[ -n \"$CONFIG\" ]] && [[ -f \"$CONFIG\" ]]; then\n # Extract patterns from YAML (POSIX-compatible, no grep -P)\n SENS_PAT=$(grep -A 20 \"^sensitive_name_regex:\" \"$CONFIG\" | sed -n 's/.*\"\\([^\"]*\\)\".*/\\1/p' | head -1 || echo \"\")\n if [[ -n \"$SENS_PAT\" ]]; then\n SENSITIVE_PATTERN=\"$SENS_PAT\"\n fi\n\n SEC_FUNCS=$(grep -A 20 \"^secure_heap_alloc_funcs:\" \"$CONFIG\" | sed -n 's/.*- \"\\([^\"]*\\)\".*/\\1/p' | tr '\\n' '|' | sed 's/|$//')\n if [[ -n \"$SEC_FUNCS\" ]]; then\n SECURE_ALLOC_FUNCS=\"($SEC_FUNCS)\"\n elif [[ -z \"$SENS_PAT\" ]]; then\n echo \"WARNING: config file provided but no patterns extracted from $CONFIG\" >&2\n fi\nfi\n\n# Arrays to collect findings\nINSECURE_ALLOCS=()\nSECURE_ALLOCS=()\nMISSING_MLOCK=()\nMISSING_MADVISE=()\nMADVISE_RE='madvise[[:space:]]*\\(([a-zA-Z_][a-zA-Z0-9_]*)[^)]*MADV_(DONTDUMP|DONTFORK|WIPEONFORK)'\n\n# Track allocated pointers to check for mlock/madvise\ndeclare -A ALLOCATED_PTRS\n\nLINE_NUM=0\n\nwhile IFS= read -r line; do\n ((LINE_NUM++))\n\n # Skip comments\n [[ \"$line\" =~ ^[[:space:]]*// ]] && continue\n [[ \"$line\" =~ ^[[:space:]]*\\* ]] && continue\n\n # Detect insecure allocations\n if [[ \"$line\" =~ ([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*(malloc|calloc|realloc)[[:space:]]*\\( ]]; then\n PTR=\"${BASH_REMATCH[1]}\"\n ALLOC_FUNC=\"${BASH_REMATCH[2]}\"\n\n if [[ \"$PTR\" =~ $SENSITIVE_PATTERN ]]; then\n INSECURE_ALLOCS+=(\"{\\\"line\\\": $LINE_NUM, \\\"pointer\\\": \\\"$PTR\\\", \\\"allocator\\\": \\\"$ALLOC_FUNC\\\", \\\"severity\\\": \\\"high\\\", \\\"context\\\": \\\"$(json_escape \"$line\")\\\"}\")\n ALLOCATED_PTRS[\"$PTR\"]=\"insecure:$LINE_NUM\"\n fi\n fi\n\n # Detect secure allocations\n if [[ \"$line\" =~ ([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*($SECURE_ALLOC_FUNCS)[[:space:]]*\\( ]]; then\n PTR=\"${BASH_REMATCH[1]}\"\n ALLOC_FUNC=\"${BASH_REMATCH[2]}\"\n\n SECURE_ALLOCS+=(\"{\\\"line\\\": $LINE_NUM, \\\"pointer\\\": \\\"$PTR\\\", \\\"allocator\\\": \\\"$ALLOC_FUNC\\\", \\\"context\\\": \\\"$(json_escape \"$line\")\\\"}\")\n ALLOCATED_PTRS[\"$PTR\"]=\"secure:$LINE_NUM\"\n fi\n\n # Detect mlock usage\n if [[ \"$line\" =~ mlock[2]?[[:space:]]*\\(([a-zA-Z_][a-zA-Z0-9_]*) ]]; then\n PTR=\"${BASH_REMATCH[1]}\"\n if [[ -n \"${ALLOCATED_PTRS[$PTR]:-}\" ]]; then\n ALLOCATED_PTRS[\"$PTR\"]=\"${ALLOCATED_PTRS[$PTR]}:mlocked\"\n fi\n fi\n\n # Detect madvise usage\n if [[ \"$line\" =~ $MADVISE_RE ]]; then\n PTR=\"${BASH_REMATCH[1]}\"\n if [[ -n \"${ALLOCATED_PTRS[$PTR]:-}\" ]]; then\n ALLOCATED_PTRS[\"$PTR\"]=\"${ALLOCATED_PTRS[$PTR]}:madvised\"\n fi\n fi\n\ndone \u003c\"$SRC\"\n\n# Check for missing protections\nfor PTR in \"${!ALLOCATED_PTRS[@]}\"; do\n INFO=\"${ALLOCATED_PTRS[$PTR]}\"\n\n if [[ \"$INFO\" =~ ^insecure: ]]; then\n LINE=\"${INFO#insecure:}\"\n LINE=\"${LINE%%:*}\"\n\n if [[ ! \"$INFO\" =~ mlocked ]]; then\n MISSING_MLOCK+=(\"{\\\"line\\\": $LINE, \\\"pointer\\\": \\\"$PTR\\\", \\\"recommendation\\\": \\\"Add mlock() to prevent swapping to disk\\\"}\")\n fi\n\n if [[ ! \"$INFO\" =~ madvised ]]; then\n MISSING_MADVISE+=(\"{\\\"line\\\": $LINE, \\\"pointer\\\": \\\"$PTR\\\", \\\"recommendation\\\": \\\"Add madvise(MADV_DONTDUMP) to exclude from core dumps\\\"}\")\n fi\n fi\ndone\n\n# Generate JSON report\nmkdir -p \"$(dirname \"$OUT\")\"\n\ncat >\"$OUT\" \u003c\u003cEOF\n{\n \"source_file\": \"$SRC\",\n \"findings\": {\n \"insecure_allocations\": [\n $(\n IFS=,\n echo \"${INSECURE_ALLOCS[*]}\"\n)\n ],\n \"secure_allocations\": [\n $(\n IFS=,\n echo \"${SECURE_ALLOCS[*]}\"\n)\n ],\n \"missing_mlock\": [\n $(\n IFS=,\n echo \"${MISSING_MLOCK[*]}\"\n)\n ],\n \"missing_madvise\": [\n $(\n IFS=,\n echo \"${MISSING_MADVISE[*]}\"\n)\n ]\n },\n \"summary\": {\n \"insecure_alloc_count\": ${#INSECURE_ALLOCS[@]},\n \"secure_alloc_count\": ${#SECURE_ALLOCS[@]},\n \"missing_protection_count\": $((${#MISSING_MLOCK[@]} + ${#MISSING_MADVISE[@]}))\n },\n \"recommendations\": [\n \"Replace malloc/calloc/realloc with OPENSSL_secure_malloc/sodium_malloc for sensitive data\",\n \"Use mlock() to prevent sensitive memory from being swapped to disk\",\n \"Use madvise(MADV_DONTDUMP) to exclude sensitive memory from core dumps\",\n \"Use madvise(MADV_WIPEONFORK) to zero memory in child processes after fork\"\n ]\n}\nEOF\n\n# Validate JSON output\nif command -v jq &>/dev/null; then\n if ! jq empty \"$OUT\" 2>/dev/null; then\n echo \"ERROR: generated JSON is malformed: $OUT\" >&2\n exit 1\n fi\nfi\n\necho \"OK: heap analysis written to $OUT\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5580,"content_sha256":"ad7d83bd10fa9968d76922c63cbd41256fcbb93f18e3d4d6cf8e8310aa8ad5d9"},{"filename":"tools/analyze_ir_semantic.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\nSemantic LLVM IR analyzer for zeroization detection.\n\nThis tool parses LLVM IR structurally (not just regex) to detect:\n- Memory operations in SSA form (mem2reg output)\n- Loop-unrolled zeroization patterns\n- Complex optimization transformations\n- Store/load chains that affect zeroization\n\"\"\"\n\nimport argparse\nimport json\nimport re\nimport sys\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\n\n\n@dataclass\nclass IRInstruction:\n \"\"\"Represents an LLVM IR instruction.\"\"\"\n\n line_num: int\n opcode: str\n operands: list[str]\n result: str | None\n raw_line: str\n metadata: dict[str, str] = field(default_factory=dict)\n\n\n@dataclass\nclass BasicBlock:\n \"\"\"Represents a basic block in LLVM IR.\"\"\"\n\n label: str\n instructions: list[IRInstruction]\n successors: list[str] = field(default_factory=list)\n predecessors: list[str] = field(default_factory=list)\n\n\n@dataclass\nclass Function:\n \"\"\"Represents a function in LLVM IR.\"\"\"\n\n name: str\n basic_blocks: dict[str, BasicBlock]\n entry_block: str | None = None\n arguments: list[str] = field(default_factory=list)\n\n\nclass SemanticIRAnalyzer:\n \"\"\"Semantic analyzer for LLVM IR.\"\"\"\n\n def __init__(self, ir_file: Path, config: dict):\n self.ir_file = ir_file\n self.config = config\n self.functions: dict[str, Function] = {}\n self.current_function: Function | None = None\n self.current_block: BasicBlock | None = None\n\n def parse_ir(self) -> None:\n \"\"\"Parse LLVM IR file into structured representation.\"\"\"\n with open(self.ir_file) as f:\n lines = f.readlines()\n\n line_num = 0\n for line in lines:\n line_num += 1\n line = line.strip()\n\n # Skip comments and empty lines\n if not line or line.startswith(\";\"):\n continue\n\n # Function definition\n if line.startswith(\"define \"):\n self._parse_function_def(line)\n continue\n\n # Function end\n if line == \"}\" and self.current_function:\n self.functions[self.current_function.name] = self.current_function\n self.current_function = None\n self.current_block = None\n continue\n\n # Basic block label\n if self.current_function and \":\" in line and not line.startswith(\"%\"):\n label = line.split(\":\")[0].strip()\n self.current_block = BasicBlock(label=label, instructions=[])\n self.current_function.basic_blocks[label] = self.current_block\n if not self.current_function.entry_block:\n self.current_function.entry_block = label\n continue\n\n # Instruction\n if self.current_function and self.current_block:\n inst = self._parse_instruction(line, line_num)\n if inst:\n self.current_block.instructions.append(inst)\n\n # Track control flow\n if inst.opcode in [\"br\", \"switch\", \"ret\"]:\n self._update_control_flow(inst)\n\n def _parse_function_def(self, line: str) -> None:\n \"\"\"Parse function definition.\"\"\"\n # Extract function name: define ... @func_name(...)\n match = re.search(r\"@([a-zA-Z0-9_\\.]+)\\s*\\(\", line)\n if match:\n func_name = match.group(1)\n self.current_function = Function(name=func_name, basic_blocks={})\n\n # Extract arguments\n args_match = re.search(r\"\\((.*?)\\)\", line)\n if args_match:\n args_str = args_match.group(1)\n # Simple argument parsing (just count for now)\n self.current_function.arguments = [\n arg.strip() for arg in args_str.split(\",\") if arg.strip()\n ]\n\n def _parse_instruction(self, line: str, line_num: int) -> IRInstruction | None:\n \"\"\"Parse single instruction.\"\"\"\n # Pattern: %result = opcode operands\n # or: opcode operands (for void instructions)\n\n result = None\n rest = line\n\n if \"=\" in line:\n parts = line.split(\"=\", 1)\n result = parts[0].strip()\n rest = parts[1].strip()\n\n # Extract opcode\n tokens = rest.split(None, 1)\n if not tokens:\n return None\n\n opcode = tokens[0]\n operands_str = tokens[1] if len(tokens) > 1 else \"\"\n\n # Parse operands (simplified)\n operands = self._parse_operands(operands_str)\n\n return IRInstruction(\n line_num=line_num, opcode=opcode, operands=operands, result=result, raw_line=line\n )\n\n def _parse_operands(self, operands_str: str) -> list[str]:\n \"\"\"Parse instruction operands.\"\"\"\n # Simple tokenization (can be improved)\n operands = []\n current = \"\"\n depth = 0\n\n for char in operands_str:\n if char in \"([{\":\n depth += 1\n elif char in \")]}\":\n depth -= 1\n elif char == \",\" and depth == 0:\n if current.strip():\n operands.append(current.strip())\n current = \"\"\n continue\n current += char\n\n if current.strip():\n operands.append(current.strip())\n\n return operands\n\n def _update_control_flow(self, inst: IRInstruction) -> None:\n \"\"\"Update CFG based on control flow instruction.\"\"\"\n if not self.current_block:\n return\n\n if inst.opcode == \"br\":\n # Conditional: br i1 %cond, label %true, label %false\n # Unconditional: br label %target\n labels = [\n op.replace(\"label\", \"\").replace(\"%\", \"\").strip()\n for op in inst.operands\n if \"label\" in op\n ]\n self.current_block.successors.extend(labels)\n\n # Update predecessors\n for label in labels:\n if label in self.current_function.basic_blocks:\n self.current_function.basic_blocks[label].predecessors.append(\n self.current_block.label\n )\n\n elif inst.opcode == \"switch\":\n # switch i32 %val, label %default [ ... cases ... ]\n labels = [\n op.replace(\"label\", \"\").replace(\"%\", \"\").strip()\n for op in inst.operands\n if \"label\" in op\n ]\n self.current_block.successors.extend(labels)\n\n def find_memory_operations(self, func: Function) -> dict[str, list[IRInstruction]]:\n \"\"\"Find all memory operations (load, store, memset, memcpy, etc.).\"\"\"\n mem_ops = {\"store\": [], \"load\": [], \"memset\": [], \"memcpy\": [], \"call\": []}\n\n for bb in func.basic_blocks.values():\n for inst in bb.instructions:\n if inst.opcode == \"store\":\n mem_ops[\"store\"].append(inst)\n elif inst.opcode == \"load\":\n mem_ops[\"load\"].append(inst)\n elif inst.opcode == \"call\":\n # Check for memset/memcpy/zeroize calls\n call_target = self._extract_call_target(inst)\n if \"memset\" in call_target or \"llvm.memset\" in call_target:\n mem_ops[\"memset\"].append(inst)\n elif \"memcpy\" in call_target or \"llvm.memcpy\" in call_target:\n mem_ops[\"memcpy\"].append(inst)\n elif any(\n fn in call_target\n for fn in [\"explicit_bzero\", \"OPENSSL_cleanse\", \"sodium_memzero\", \"zeroize\"]\n ):\n mem_ops[\"call\"].append(inst)\n\n return mem_ops\n\n def _extract_call_target(self, inst: IRInstruction) -> str:\n \"\"\"Extract function name from call instruction.\"\"\"\n for op in inst.operands:\n if \"@\" in op:\n match = re.search(r\"@([a-zA-Z0-9_\\.]+)\", op)\n if match:\n return match.group(1)\n return \"\"\n\n def detect_loop_unrolled_wipes(self, func: Function) -> list[dict]:\n \"\"\"Detect zeroization patterns from loop unrolling.\"\"\"\n findings = []\n\n for bb_label, bb in func.basic_blocks.items():\n # Look for patterns like:\n # store i8 0, i8* %ptr.0\n # store i8 0, i8* %ptr.1\n # store i8 0, i8* %ptr.2\n # ... (repeated pattern indicating unrolled loop)\n\n zero_stores = []\n for inst in bb.instructions:\n # Check if storing 0\n if (\n inst.opcode == \"store\"\n and inst.operands\n and (\"i8 0\" in inst.operands[0] or \"i32 0\" in inst.operands[0])\n ):\n zero_stores.append(inst)\n\n # If we have 4+ consecutive zero stores, likely an unrolled wipe loop\n if len(zero_stores) >= 4:\n # Check if addresses are sequential\n addresses = [self._extract_store_address(inst) for inst in zero_stores]\n if self._are_sequential_addresses(addresses):\n findings.append(\n {\n \"type\": \"LOOP_UNROLLED_WIPE\",\n \"block\": bb_label,\n \"count\": len(zero_stores),\n \"first_line\": zero_stores[0].line_num,\n \"evidence\": (\n f\"Found {len(zero_stores)} consecutive zero stores\"\n \" (likely unrolled loop)\"\n ),\n }\n )\n\n return findings\n\n def _extract_store_address(self, inst: IRInstruction) -> str:\n \"\"\"Extract address operand from store instruction.\"\"\"\n # store type value, type* pointer\n if len(inst.operands) >= 2:\n return inst.operands[1]\n return \"\"\n\n def _are_sequential_addresses(self, addresses: list[str]) -> bool:\n \"\"\"Check if addresses look sequential (e.g., %ptr.0, %ptr.1, %ptr.2).\"\"\"\n if len(addresses) \u003c 2:\n return False\n\n # Simple heuristic: check for pattern like %name.0, %name.1, etc.\n base_pattern = re.sub(r\"\\d+\", \"\", addresses[0])\n return all(re.sub(r\"\\d+\", \"\", addr) == base_pattern for addr in addresses[1:])\n\n def detect_volatile_stores(self, func: Function) -> list[IRInstruction]:\n \"\"\"Find volatile store instructions (cannot be optimized away).\"\"\"\n volatile_stores = []\n\n for bb in func.basic_blocks.values():\n for inst in bb.instructions:\n if inst.opcode == \"store\" and \"volatile\" in inst.raw_line:\n volatile_stores.append(inst)\n\n return volatile_stores\n\n def analyze_mem2reg_output(self, func: Function) -> dict:\n \"\"\"Analyze memory operations in SSA form (after mem2reg pass).\"\"\"\n # After mem2reg, local variables are promoted to registers\n # Look for phi nodes and register operations\n\n phi_nodes = []\n register_ops = []\n\n for bb in func.basic_blocks.values():\n for inst in bb.instructions:\n if inst.opcode == \"phi\":\n phi_nodes.append(inst)\n elif inst.result and inst.result.startswith(\"%\"):\n register_ops.append(inst)\n\n return {\n \"phi_count\": len(phi_nodes),\n \"register_ops\": len(register_ops),\n \"has_mem2reg\": len(phi_nodes) > 0,\n }\n\n def analyze_function(self, func_name: str) -> dict:\n \"\"\"Perform comprehensive analysis on a function.\"\"\"\n if func_name not in self.functions:\n return {\"error\": f\"Function {func_name} not found\"}\n\n func = self.functions[func_name]\n\n # Find memory operations\n mem_ops = self.find_memory_operations(func)\n\n # Detect patterns\n loop_unrolled = self.detect_loop_unrolled_wipes(func)\n volatile_stores = self.detect_volatile_stores(func)\n mem2reg_info = self.analyze_mem2reg_output(func)\n\n # Check for wipe presence\n has_wipe = (\n len(mem_ops[\"memset\"]) > 0 or len(mem_ops[\"call\"]) > 0 or len(volatile_stores) > 0\n )\n\n return {\n \"function\": func_name,\n \"basic_blocks\": len(func.basic_blocks),\n \"memory_operations\": {\n \"stores\": len(mem_ops[\"store\"]),\n \"loads\": len(mem_ops[\"load\"]),\n \"memset_calls\": len(mem_ops[\"memset\"]),\n \"secure_wipe_calls\": len(mem_ops[\"call\"]),\n \"volatile_stores\": len(volatile_stores),\n },\n \"patterns\": {\n \"loop_unrolled_wipes\": loop_unrolled,\n \"has_volatile_stores\": len(volatile_stores) > 0,\n },\n \"ssa_analysis\": mem2reg_info,\n \"has_zeroization\": has_wipe,\n \"wipe_instructions\": [\n {\"line\": inst.line_num, \"type\": \"memset\", \"raw\": inst.raw_line}\n for inst in mem_ops[\"memset\"]\n ]\n + [\n {\"line\": inst.line_num, \"type\": \"secure_call\", \"raw\": inst.raw_line}\n for inst in mem_ops[\"call\"]\n ]\n + [\n {\"line\": inst.line_num, \"type\": \"volatile_store\", \"raw\": inst.raw_line}\n for inst in volatile_stores\n ],\n }\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Semantic LLVM IR analyzer\")\n parser.add_argument(\"--ir\", required=True, help=\"LLVM IR file (.ll)\")\n parser.add_argument(\"--function\", help=\"Specific function to analyze (default: all)\")\n parser.add_argument(\"--config\", help=\"Configuration YAML file\")\n parser.add_argument(\"--out\", required=True, help=\"Output JSON file\")\n\n args = parser.parse_args()\n\n # Load config (simplified)\n config = {}\n\n # Parse IR\n analyzer = SemanticIRAnalyzer(Path(args.ir), config)\n try:\n analyzer.parse_ir()\n except OSError as e:\n print(f\"Error: cannot read IR file {args.ir}: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # Analyze functions\n results = {\"ir_file\": args.ir, \"functions_found\": len(analyzer.functions), \"analyses\": []}\n\n if args.function:\n # Analyze specific function\n analysis = analyzer.analyze_function(args.function)\n results[\"analyses\"].append(analysis)\n else:\n # Analyze all functions\n for func_name in analyzer.functions:\n analysis = analyzer.analyze_function(func_name)\n results[\"analyses\"].append(analysis)\n\n # Write output\n output_path = Path(args.out)\n output_path.parent.mkdir(parents=True, exist_ok=True)\n\n with open(output_path, \"w\") as f:\n json.dump(results, f, indent=2)\n\n print(f\"OK: semantic IR analysis written to {args.out}\")\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15149,"content_sha256":"202a002ce09f5d0e1cccbf3a7791231a61795b6e2f4dcf2d384151278a0d788a"},{"filename":"tools/diff_ir.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# Normalize and diff LLVM IR across one or more optimization levels.\n#\n# Usage (two-file, backward-compatible):\n# diff_ir.sh \u003cO0.ll> \u003cO2.ll>\n#\n# Usage (multi-level — recommended):\n# diff_ir.sh \u003cO0.ll> \u003cO1.ll> \u003cO2.ll> [\u003cO3.ll> ...]\n#\n# Output:\n# - Prints a unified diff for each pair of adjacent files.\n# - For 3+ files, also prints a WIPE PATTERN SUMMARY identifying the first\n# optimization level at which zeroization patterns disappear.\n# - Returns exit code 0 if all files are identical, 1 if any diffs found.\n#\n# Wipe patterns detected in the summary:\n# llvm.memset, volatile, explicit_bzero, sodium_memzero, OPENSSL_cleanse,\n# SecureZeroMemory, memset_s, store i8 0, store i64 0, store i32 0\n\nusage() {\n echo \"Usage: $0 \u003cbaseline.ll> \u003cfile2.ll> [\u003cfile3.ll> ...]\" >&2\n}\n\nif [[ $# -lt 2 ]]; then\n usage\n exit 2\nfi\n\nfor f in \"$@\"; do\n if [[ ! -f \"$f\" ]]; then\n echo \"Missing file: $f\" >&2\n exit 2\n fi\ndone\n\nnorm() {\n # Remove comments and metadata noise that changes frequently.\n # Keep it simple and safe: do NOT rewrite semantics, only strip obviously noisy lines.\n sed -E \\\n -e 's/;.*$//' \\\n -e '/^\\s*$/d' \\\n -e '/^source_filename = /d' \\\n -e '/^target datalayout = /d' \\\n -e '/^target triple = /d' \\\n -e '/^!llvm\\./d' \\\n -e '/^!DIGlobalVariable/d' \\\n -e '/^!DICompileUnit/d' \\\n -e '/^!DIFile/d' \\\n -e '/^!DISubprogram/d' \\\n -e '/^!DILocation/d' \\\n -e '/^!DI.*$/d'\n}\n\nhas_wipe_pattern() {\n # Return 0 (true) if the file contains any zeroization pattern.\n grep -qE \\\n 'llvm\\.memset|volatile|explicit_bzero|sodium_memzero|OPENSSL_cleanse|SecureZeroMemory|memset_s|store i8 0|store i64 0|store i32 0' \\\n \"$1\"\n}\n\n# ---------------------------------------------------------------------------\n# Normalize all input files into temp files.\n# ---------------------------------------------------------------------------\nFILES=(\"$@\")\nNUM_FILES=${#FILES[@]}\n\nTMPDIR_BASE=\"$(mktemp -d -t za-ir-XXXXXX)\"\ntrap 'rm -rf \"$TMPDIR_BASE\"' EXIT\n\nNORMFILES=()\nfor i in \"${!FILES[@]}\"; do\n tmp=\"$TMPDIR_BASE/norm_${i}.ll\"\n norm \u003c\"${FILES[$i]}\" >\"$tmp\"\n NORMFILES+=(\"$tmp\")\ndone\n\n# ---------------------------------------------------------------------------\n# Two-file mode: backward-compatible, single diff, no summary.\n# ---------------------------------------------------------------------------\nif [[ $NUM_FILES -eq 2 ]]; then\n diff_rc=0\n diff -u \"${NORMFILES[0]}\" \"${NORMFILES[1]}\" || diff_rc=$?\n if [[ $diff_rc -eq 2 ]]; then\n echo \"diff_ir.sh: diff failed (internal error)\" >&2\n exit 1\n fi\n exit $diff_rc\nfi\n\n# ---------------------------------------------------------------------------\n# Multi-file mode: pairwise diffs between adjacent files + wipe summary.\n# ---------------------------------------------------------------------------\nany_diff=0\n\nfor ((i = 0; i \u003c NUM_FILES - 1; i++)); do\n j=$((i + 1))\n A_LABEL=\"$(basename \"${FILES[$i]}\")\"\n B_LABEL=\"$(basename \"${FILES[$j]}\")\"\n echo \"=== DIFF File $((i + 1)) ($A_LABEL) vs File $((j + 1)) ($B_LABEL) ===\"\n if ! diff -u \"${NORMFILES[$i]}\" \"${NORMFILES[$j]}\"; then\n any_diff=1\n fi\n echo \"\"\ndone\n\n# ---------------------------------------------------------------------------\n# Wipe pattern summary: identify first file where wipe disappears.\n# ---------------------------------------------------------------------------\necho \"=== WIPE PATTERN SUMMARY ===\"\nfirst_absent=-1\nfor i in \"${!NORMFILES[@]}\"; do\n LABEL=\"$(basename \"${FILES[$i]}\")\"\n if has_wipe_pattern \"${NORMFILES[$i]}\"; then\n echo \" File $((i + 1)) ($LABEL): WIPE PRESENT\"\n else\n echo \" File $((i + 1)) ($LABEL): WIPE ABSENT\"\n if [[ $first_absent -eq -1 ]]; then\n first_absent=$i\n fi\n fi\ndone\n\nif [[ $first_absent -ne -1 ]]; then\n LABEL=\"$(basename \"${FILES[$first_absent]}\")\"\n echo \"\"\n echo \" First disappearance at File $((first_absent + 1)) ($LABEL).\"\n echo \" Evidence: OPTIMIZED_AWAY_ZEROIZE — wipe present at lower opt level(s) but absent here.\"\nelse\n echo \"\"\n echo \" Wipe patterns present at all opt levels analyzed.\"\nfi\n\nexit $any_diff\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4111,"content_sha256":"2fa82d280acbab6aeffad7cd11fe88dd5388c2ad9df50b4a22cc0529e1687ec7"},{"filename":"tools/diff_rust_mir.sh","content":"#!/usr/bin/env bash\n# diff_rust_mir.sh — Normalize and diff Rust MIR across optimization levels.\n#\n# Compares MIR output from different optimization levels to detect zeroize-\n# related transformations: drop glue removal, StorageDead elimination, and\n# zeroize call elimination.\n#\n# Exit codes:\n# 0 all files are identical after normalization\n# 1 at least one diff found (or wipe patterns disappeared)\n# 2 argument error\n#\n# Usage (two-file, backward-compatible):\n# diff_rust_mir.sh \u003cO0.mir> \u003cO2.mir>\n#\n# Usage (multi-level — recommended):\n# diff_rust_mir.sh \u003cO0.mir> \u003cO1.mir> \u003cO2.mir> [\u003cO3.mir> ...]\n#\n# Output:\n# - Unified diff for each pair of adjacent files.\n# - For 3+ files, a ZEROIZE PATTERN SUMMARY identifying the first opt level\n# at which patterns disappear.\n#\n# Wipe patterns detected:\n# zeroize::, Zeroize::zeroize, volatile_set_memory, drop_in_place,\n# StorageDead for sensitive locals, ptr::write_bytes\n\nset -euo pipefail\n\nusage() {\n cat \u003c\u003c'EOF'\nUsage:\n diff_rust_mir.sh \u003cbaseline.mir> \u003cfile2.mir> [\u003cfile3.mir> ...]\n\nCompares Rust MIR files across optimization levels. Normalizes away noisy\nmetadata (source locations, scope info, storage annotations) and diffs\nthe semantic content. Detects disappearance of zeroize-related patterns.\n\nExamples:\n diff_rust_mir.sh crate.O0.mir crate.O2.mir\n diff_rust_mir.sh crate.O0.mir crate.O1.mir crate.O2.mir crate.O3.mir\nEOF\n}\n\nif [[ $# -lt 2 ]]; then\n usage\n exit 2\nfi\n\nfor f in \"$@\"; do\n if [[ ! -f \"$f\" ]]; then\n echo \"diff_rust_mir.sh: missing file: $f\" >&2\n exit 2\n fi\ndone\n\n# ---------------------------------------------------------------------------\n# Normalization: strip noisy metadata that changes between opt levels\n# but is semantically irrelevant for zeroize analysis.\n# ---------------------------------------------------------------------------\nnorm() {\n sed -E \\\n -e '/^\\/\\/ WARNING:/d' \\\n -e '/^\\/\\/ MIR for/d' \\\n -e 's/scope [0-9]+ at [^ ]+:[0-9]+:[0-9]+/scope N at \u003cloc>/g' \\\n -e 's/at [^ ]+\\.rs:[0-9]+:[0-9]+/at \u003cloc>/g' \\\n -e 's/\\/\\/ .*$//g' \\\n -e '/^\\s*$/d'\n}\n\n# ---------------------------------------------------------------------------\n# Pattern detection: Rust MIR zeroize-related constructs\n# ---------------------------------------------------------------------------\nhas_zeroize_pattern() {\n grep -qE \\\n 'zeroize::|Zeroize::zeroize|volatile_set_memory|ptr::write_bytes|drop_in_place.*[Kk]ey|drop_in_place.*[Ss]ecret|drop_in_place.*[Pp]assword|drop_in_place.*[Tt]oken|drop_in_place.*[Nn]once|drop_in_place.*[Ss]eed|drop_in_place.*[Pp]riv|Zeroizing|ZeroizeOnDrop' \\\n \"$1\"\n}\n\nhas_drop_glue() {\n grep -qE 'drop_in_place|drop\\(_[0-9]+\\)' \"$1\"\n}\n\n# shellcheck disable=SC2329,SC2317 # invoked indirectly by agent prompts\nhas_storage_dead_sensitive() {\n grep -qE 'StorageDead\\(_[0-9]+\\)' \"$1\" &&\n grep -qE '(key|secret|password|token|nonce|seed|priv|master|credential)' \"$1\"\n}\n\n# ---------------------------------------------------------------------------\n# Setup\n# ---------------------------------------------------------------------------\nFILES=(\"$@\")\nNUM_FILES=${#FILES[@]}\n\nTMPDIR_BASE=\"$(mktemp -d -t za-mir-XXXXXX)\"\ntrap 'rm -rf \"$TMPDIR_BASE\"' EXIT\n\nNORMFILES=()\nfor i in \"${!FILES[@]}\"; do\n tmp=\"$TMPDIR_BASE/norm_${i}.mir\"\n norm \u003c\"${FILES[$i]}\" >\"$tmp\"\n NORMFILES+=(\"$tmp\")\ndone\n\n# ---------------------------------------------------------------------------\n# Two-file mode: backward-compatible, single diff, no summary.\n# ---------------------------------------------------------------------------\nif [[ $NUM_FILES -eq 2 ]]; then\n diff_rc=0\n diff -u \"${NORMFILES[0]}\" \"${NORMFILES[1]}\" || diff_rc=$?\n if [[ $diff_rc -eq 2 ]]; then\n echo \"diff_rust_mir.sh: diff failed (internal error)\" >&2\n exit 1\n fi\n exit $diff_rc\nfi\n\n# ---------------------------------------------------------------------------\n# Multi-file mode: pairwise diffs + zeroize pattern summary.\n# ---------------------------------------------------------------------------\nany_diff=0\n\nfor ((i = 0; i \u003c NUM_FILES - 1; i++)); do\n j=$((i + 1))\n A_LABEL=\"$(basename \"${FILES[$i]}\")\"\n B_LABEL=\"$(basename \"${FILES[$j]}\")\"\n echo \"=== DIFF File $((i + 1)) ($A_LABEL) vs File $((j + 1)) ($B_LABEL) ===\"\n if ! diff -u --label \"$A_LABEL\" --label \"$B_LABEL\" \\\n \"${NORMFILES[$i]}\" \"${NORMFILES[$j]}\"; then\n any_diff=1\n fi\n echo \"\"\ndone\n\n# ---------------------------------------------------------------------------\n# Zeroize pattern summary\n# ---------------------------------------------------------------------------\necho \"=== ZEROIZE PATTERN SUMMARY ===\"\nfirst_absent=-1\nfor i in \"${!NORMFILES[@]}\"; do\n LABEL=\"$(basename \"${FILES[$i]}\")\"\n if has_zeroize_pattern \"${NORMFILES[$i]}\"; then\n echo \" File $((i + 1)) ($LABEL): ZEROIZE CALLS PRESENT\"\n else\n echo \" File $((i + 1)) ($LABEL): ZEROIZE CALLS ABSENT\"\n if [[ $first_absent -eq -1 ]]; then\n first_absent=$i\n fi\n fi\ndone\necho \"\"\n\n# ---------------------------------------------------------------------------\n# Drop glue summary\n# ---------------------------------------------------------------------------\necho \"=== DROP GLUE SUMMARY ===\"\nfirst_drop_absent=-1\nfor i in \"${!NORMFILES[@]}\"; do\n LABEL=\"$(basename \"${FILES[$i]}\")\"\n if has_drop_glue \"${NORMFILES[$i]}\"; then\n echo \" File $((i + 1)) ($LABEL): DROP GLUE PRESENT\"\n else\n echo \" File $((i + 1)) ($LABEL): DROP GLUE ABSENT\"\n if [[ $first_drop_absent -eq -1 ]]; then\n first_drop_absent=$i\n fi\n fi\ndone\necho \"\"\n\n# ---------------------------------------------------------------------------\n# Verdict\n# ---------------------------------------------------------------------------\nif [[ $first_absent -ne -1 ]]; then\n LABEL=\"$(basename \"${FILES[$first_absent]}\")\"\n echo \"WARNING: Zeroize patterns first disappear at File $((first_absent + 1)) ($LABEL).\"\n echo \" Evidence: OPTIMIZED_AWAY_ZEROIZE — zeroize calls present at lower opt level(s) but absent here.\"\n any_diff=1\nelif [[ $first_drop_absent -ne -1 ]]; then\n LABEL=\"$(basename \"${FILES[$first_drop_absent]}\")\"\n echo \"WARNING: Drop glue first disappears at File $((first_drop_absent + 1)) ($LABEL).\"\n echo \" Evidence: Drop glue present at lower opt level(s) but absent here — sensitive type drop may be inlined or elided.\"\n any_diff=1\nelse\n echo \"OK: Zeroize patterns and drop glue present at all opt levels analyzed.\"\nfi\n\nexit $any_diff\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":6397,"content_sha256":"2d383e50a3ccca17f5a8e5af26f08241cd5811e330f9e5d2ce9e5a249778cbdd"},{"filename":"tools/emit_asm.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# Emit assembly for a given translation unit.\n#\n# Usage:\n# emit_asm.sh --cc clang --src path/to/file.c --out /tmp/file.s --opt O2 -- \u003cextra compile args>\n\nusage() {\n echo \"Usage: $0 --src \u003cfile> --out \u003cout.s> [--cc clang] [--opt O0|O1|O2|O3|Os|Oz] -- \u003cextra args>\" >&2\n}\n\nCC=\"clang\"\nSRC=\"\"\nOUT=\"\"\nOPT=\"O0\"\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --cc)\n CC=\"$2\"\n shift 2\n ;;\n --src)\n SRC=\"$2\"\n shift 2\n ;;\n --out)\n OUT=\"$2\"\n shift 2\n ;;\n --opt)\n OPT=\"$2\"\n shift 2\n ;;\n --)\n shift\n break\n ;;\n *)\n echo \"Unknown arg: $1\" >&2\n usage\n exit 2\n ;;\n esac\ndone\n\nif [[ -z \"$SRC\" || -z \"$OUT\" ]]; then\n usage\n exit 2\nfi\n\ncase \"$OPT\" in\n O0 | O1 | O2 | O3 | Os | Oz) ;;\n *)\n echo \"Invalid --opt: $OPT\" >&2\n usage\n exit 2\n ;;\nesac\n\nEXTRA=(\"$@\")\nmkdir -p \"$(dirname \"$OUT\")\"\n\n\"$CC\" \"-$OPT\" -S \"$SRC\" -o \"$OUT\" ${EXTRA[@]+\"${EXTRA[@]}\"}\n\necho \"OK: wrote asm to $OUT\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1026,"content_sha256":"af4db5b52071eb52f1f5579438c199142453e9fb6e6dd775c31a3edd9ea5f34e"},{"filename":"tools/emit_ir.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# Emit LLVM IR for a given translation unit.\n#\n# Usage:\n# emit_ir.sh --cc clang --src path/to/file.c --out /tmp/file.ll --opt O2 -- \u003cextra compile args>\n#\n# Notes:\n# - Use `--` to pass through extra include/define flags.\n# - We intentionally do not attempt to parse compile_commands.json here.\n# Your runner should extract the TU command and pass flags after `--`.\n\nusage() {\n echo \"Usage: $0 --src \u003cfile> --out \u003cout.ll> [--cc clang] [--opt O0|O1|O2|O3|Os|Oz] -- \u003cextra args>\" >&2\n}\n\nCC=\"clang\"\nSRC=\"\"\nOUT=\"\"\nOPT=\"O0\"\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --cc)\n CC=\"$2\"\n shift 2\n ;;\n --src)\n SRC=\"$2\"\n shift 2\n ;;\n --out)\n OUT=\"$2\"\n shift 2\n ;;\n --opt)\n OPT=\"$2\"\n shift 2\n ;;\n --)\n shift\n break\n ;;\n *)\n echo \"Unknown arg: $1\" >&2\n usage\n exit 2\n ;;\n esac\ndone\n\nif [[ -z \"$SRC\" || -z \"$OUT\" ]]; then\n usage\n exit 2\nfi\n\n# Normalize OPT -> clang flag\ncase \"$OPT\" in\n O0 | O1 | O2 | O3 | Os | Oz) ;;\n *)\n echo \"Invalid --opt: $OPT\" >&2\n usage\n exit 2\n ;;\nesac\n\n# Extra args after --\nEXTRA=(\"$@\")\n\n# Ensure output dir exists\nmkdir -p \"$(dirname \"$OUT\")\"\n\n# Emit IR\n\"$CC\" \"-$OPT\" -S -emit-llvm \"$SRC\" -o \"$OUT\" ${EXTRA[@]+\"${EXTRA[@]}\"}\n\necho \"OK: wrote IR to $OUT\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1340,"content_sha256":"82a3c91291ff5bc4b0ca78c3b1587aae88c1752774e0b6837c455dec5475ebf4"},{"filename":"tools/emit_rust_asm.sh","content":"#!/usr/bin/env bash\n# emit_rust_asm.sh — Emit Rust assembly for zeroize analysis.\n#\n# Exit codes:\n# 0 success\n# 1 build/output failure\n# 2 argument error\n\nset -euo pipefail\n\nusage() {\n cat \u003c\u003c'EOF'\nUsage:\n emit_rust_asm.sh --manifest \u003cCargo.toml> --out \u003cpath> [options] [-- \u003cextra cargo rustc args>]\n\nOptions:\n --manifest \u003cfile> Cargo manifest path (required)\n --out \u003cpath> Output .s file or directory (required)\n --opt \u003cO0|O1|O2|O3> Opt level (default: O2)\n --crate \u003cpkg> Workspace package (-p)\n --bin \u003ctarget> Build only a specific bin target\n --lib Build only the lib target\n --target \u003ctriple> Cross-compile target (e.g. x86_64-unknown-linux-gnu)\n --intel-syntax Emit Intel syntax instead of AT&T (default: AT&T)\n --help Show this help text\n\nExamples:\n emit_rust_asm.sh --manifest Cargo.toml --opt O2 --out /tmp/crate.O2.s\n emit_rust_asm.sh --manifest Cargo.toml --opt O0 --out /tmp/asm/ --lib\n emit_rust_asm.sh --manifest Cargo.toml --out /tmp/crate.O2.s --crate mycrate --target x86_64-unknown-linux-gnu\nEOF\n}\n\ndie_arg() {\n echo \"emit_rust_asm.sh: $*\" >&2\n exit 2\n}\n\ndie_run() {\n echo \"emit_rust_asm.sh: $*\" >&2\n exit 1\n}\n\nrequire_value() {\n local opt=\"$1\"\n local val=\"${2-}\"\n [[ -n \"$val\" ]] || die_arg \"missing value for ${opt}\"\n}\n\nMANIFEST=\"\"\nOUT=\"\"\nOPT=\"O2\"\nCRATE=\"\"\nBIN_TARGET=\"\"\nLIB_TARGET=false\nTARGET_TRIPLE=\"\"\nINTEL_SYNTAX=false\nEXTRA_ARGS=()\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --manifest)\n require_value \"$1\" \"${2-}\"\n MANIFEST=\"$2\"\n shift 2\n ;;\n --out)\n require_value \"$1\" \"${2-}\"\n OUT=\"$2\"\n shift 2\n ;;\n --opt)\n require_value \"$1\" \"${2-}\"\n OPT=\"$2\"\n shift 2\n ;;\n --crate)\n require_value \"$1\" \"${2-}\"\n CRATE=\"$2\"\n shift 2\n ;;\n --bin)\n require_value \"$1\" \"${2-}\"\n BIN_TARGET=\"$2\"\n shift 2\n ;;\n --lib)\n LIB_TARGET=true\n shift\n ;;\n --target)\n require_value \"$1\" \"${2-}\"\n TARGET_TRIPLE=\"$2\"\n shift 2\n ;;\n --intel-syntax)\n INTEL_SYNTAX=true\n shift\n ;;\n --help | -h)\n usage\n exit 0\n ;;\n --)\n shift\n EXTRA_ARGS=(\"$@\")\n break\n ;;\n *)\n die_arg \"unknown argument: $1\"\n ;;\n esac\ndone\n\n[[ -n \"$MANIFEST\" ]] || die_arg \"--manifest is required\"\n[[ -n \"$OUT\" ]] || die_arg \"--out is required\"\n[[ -f \"$MANIFEST\" ]] || die_run \"manifest not found: $MANIFEST\"\n[[ -n \"$BIN_TARGET\" && \"$LIB_TARGET\" == true ]] && die_arg \"--bin and --lib are mutually exclusive\"\n\ncase \"$OPT\" in\n O0) LEVEL=\"0\" ;;\n O1) LEVEL=\"1\" ;;\n O2) LEVEL=\"2\" ;;\n O3) LEVEL=\"3\" ;;\n *) die_arg \"unsupported opt level: $OPT (use O0, O1, O2, O3)\" ;;\nesac\n\nOUT_IS_FILE=false\nif [[ \"$OUT\" == *.s || \"$OUT\" == *.asm ]]; then\n OUT_IS_FILE=true\n mkdir -p \"$(dirname \"$OUT\")\"\nelse\n mkdir -p \"$OUT\"\nfi\n\nCARGO_ARGS=(+nightly rustc --manifest-path \"$MANIFEST\")\n[[ -n \"$CRATE\" ]] && CARGO_ARGS+=(\"-p\" \"$CRATE\")\n[[ -n \"$BIN_TARGET\" ]] && CARGO_ARGS+=(\"--bin\" \"$BIN_TARGET\")\n[[ \"$LIB_TARGET\" == true ]] && CARGO_ARGS+=(\"--lib\")\n[[ -n \"$TARGET_TRIPLE\" ]] && CARGO_ARGS+=(\"--target\" \"$TARGET_TRIPLE\")\n\nRUSTC_FLAGS=(--emit=asm -C \"opt-level=$LEVEL\")\n[[ \"$INTEL_SYNTAX\" == true ]] && RUSTC_FLAGS+=(-C \"llvm-args=-x86-asm-syntax=intel\")\n\nTARGET_DIR=\"${TMPDIR:-/tmp}/zeroize_rust_asm_${LEVEL}_$\"\nrm -rf \"$TARGET_DIR\"\nmkdir -p \"$TARGET_DIR\"\n\necho \"=== emit_rust_asm.sh ===\"\necho \"manifest: $MANIFEST\"\necho \"opt: $OPT\"\necho \"target: $TARGET_DIR\"\necho \"output: $OUT\"\n[[ -n \"$TARGET_TRIPLE\" ]] && echo \"triple: $TARGET_TRIPLE\"\n[[ \"$INTEL_SYNTAX\" == true ]] && echo \"syntax: intel\"\n\nif ! CARGO_TARGET_DIR=\"$TARGET_DIR\" cargo \"${CARGO_ARGS[@]}\" \\\n \"${EXTRA_ARGS[@]+\"${EXTRA_ARGS[@]}\"}\" \\\n -- \"${RUSTC_FLAGS[@]}\"; then\n die_run \"cargo rustc failed for opt=${OPT}\"\nfi\n\ndeclare -a ASM_FILES=()\nwhile IFS= read -r file; do\n ASM_FILES+=(\"$file\")\ndone \u003c \u003c(find \"$TARGET_DIR\" -type f -name \"*.s\" | LC_ALL=C sort)\n\n[[ \"${#ASM_FILES[@]}\" -gt 0 ]] || die_run \"no .s files found under $TARGET_DIR\"\n\nif [[ \"$OUT_IS_FILE\" == true ]]; then\n : >\"$OUT\"\n for file in \"${ASM_FILES[@]}\"; do\n cat \"$file\" >>\"$OUT\"\n done\n [[ -s \"$OUT\" ]] || die_run \"emitted assembly is empty: $OUT\"\nelse\n cp \"${ASM_FILES[@]}\" \"$OUT/\"\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4337,"content_sha256":"d23f859b9f4871a625d2b1fffd8857625c5b49a1a8b5e8f4878d0e800fb05847"},{"filename":"tools/emit_rust_ir.sh","content":"#!/usr/bin/env bash\n# emit_rust_ir.sh — Emit Rust LLVM IR for zeroize analysis.\n#\n# Exit codes:\n# 0 success\n# 1 build/output failure\n# 2 argument error\n\nset -euo pipefail\n\nusage() {\n cat \u003c\u003c'EOF'\nUsage:\n emit_rust_ir.sh --manifest \u003cCargo.toml> --out \u003cpath> [options] [-- \u003cextra cargo rustc args>]\n\nOptions:\n --manifest \u003cfile> Cargo manifest path (required)\n --out \u003cpath> Output .ll file (required)\n --opt \u003cO0|O1|O2|O3> Opt level (default: O2)\n --crate \u003cpkg> Workspace package (-p)\n --bin \u003ctarget> Build only a specific bin target\n --lib Build only the lib target\n --help Show this help text\n\nExamples:\n emit_rust_ir.sh --manifest Cargo.toml --opt O0 --out /tmp/crate.O0.ll\n emit_rust_ir.sh --manifest Cargo.toml --opt O2 --bin cli --out /tmp/cli.O2.ll\nEOF\n}\n\ndie_arg() {\n echo \"emit_rust_ir.sh: $*\" >&2\n exit 2\n}\n\ndie_run() {\n echo \"emit_rust_ir.sh: $*\" >&2\n exit 1\n}\n\nrequire_value() {\n local opt=\"$1\"\n local val=\"${2-}\"\n [[ -n \"$val\" ]] || die_arg \"missing value for ${opt}\"\n}\n\nMANIFEST=\"\"\nOUT=\"\"\nOPT=\"O2\"\nCRATE=\"\"\nBIN_TARGET=\"\"\nLIB_TARGET=false\nEXTRA_ARGS=()\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --manifest)\n require_value \"$1\" \"${2-}\"\n MANIFEST=\"$2\"\n shift 2\n ;;\n --out)\n require_value \"$1\" \"${2-}\"\n OUT=\"$2\"\n shift 2\n ;;\n --opt)\n require_value \"$1\" \"${2-}\"\n OPT=\"$2\"\n shift 2\n ;;\n --crate)\n require_value \"$1\" \"${2-}\"\n CRATE=\"$2\"\n shift 2\n ;;\n --bin)\n require_value \"$1\" \"${2-}\"\n BIN_TARGET=\"$2\"\n shift 2\n ;;\n --lib)\n LIB_TARGET=true\n shift\n ;;\n --help | -h)\n usage\n exit 0\n ;;\n --)\n shift\n EXTRA_ARGS=(\"$@\")\n break\n ;;\n *)\n die_arg \"unknown argument: $1\"\n ;;\n esac\ndone\n\n[[ -n \"$MANIFEST\" ]] || die_arg \"--manifest is required\"\n[[ -n \"$OUT\" ]] || die_arg \"--out is required\"\n[[ -f \"$MANIFEST\" ]] || die_run \"manifest not found: $MANIFEST\"\n[[ -n \"$BIN_TARGET\" && \"$LIB_TARGET\" == true ]] && die_arg \"--bin and --lib are mutually exclusive\"\n[[ \"$OUT\" == *.ll ]] || die_arg \"--out must be a .ll file path\"\n\ncase \"$OPT\" in\n O0) LEVEL=\"0\" ;;\n O1) LEVEL=\"1\" ;;\n O2) LEVEL=\"2\" ;;\n O3) LEVEL=\"3\" ;;\n *) die_arg \"unsupported opt level: $OPT (use O0, O1, O2, O3)\" ;;\nesac\n\nmkdir -p \"$(dirname \"$OUT\")\"\n\nCARGO_ARGS=(+nightly rustc --manifest-path \"$MANIFEST\")\n[[ -n \"$CRATE\" ]] && CARGO_ARGS+=(\"-p\" \"$CRATE\")\n[[ -n \"$BIN_TARGET\" ]] && CARGO_ARGS+=(\"--bin\" \"$BIN_TARGET\")\n[[ \"$LIB_TARGET\" == true ]] && CARGO_ARGS+=(\"--lib\")\n\nTARGET_DIR=\"${TMPDIR:-/tmp}/zeroize_rust_ir_${LEVEL}_$\"\nrm -rf \"$TARGET_DIR\"\nmkdir -p \"$TARGET_DIR\"\n\necho \"=== emit_rust_ir.sh ===\"\necho \"manifest: $MANIFEST\"\necho \"opt: $OPT\"\necho \"target: $TARGET_DIR\"\necho \"output: $OUT\"\n\nif ! CARGO_TARGET_DIR=\"$TARGET_DIR\" cargo \"${CARGO_ARGS[@]}\" \\\n ${EXTRA_ARGS[@]+\"${EXTRA_ARGS[@]}\"} \\\n -- --emit=llvm-ir -C opt-level=\"$LEVEL\"; then\n die_run \"cargo rustc failed for opt=${OPT}\"\nfi\n\ndeclare -a LL_FILES=()\nwhile IFS= read -r file; do\n LL_FILES+=(\"$file\")\ndone \u003c \u003c(find \"$TARGET_DIR\" -type f -name \"*.ll\" | LC_ALL=C sort)\n\n[[ \"${#LL_FILES[@]}\" -gt 0 ]] || die_run \"no .ll files found under $TARGET_DIR\"\n\n: >\"$OUT\"\nfor file in \"${LL_FILES[@]}\"; do\n cat \"$file\" >>\"$OUT\"\ndone\n\n[[ -s \"$OUT\" ]] || die_run \"emitted IR is empty: $OUT\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3411,"content_sha256":"3117475bba0a1fe350a938403597dec7a86d032a6d51494d708ae0037747dd17"},{"filename":"tools/emit_rust_mir.sh","content":"#!/usr/bin/env bash\n# emit_rust_mir.sh — Emit Rust MIR for zeroize analysis.\n#\n# Exit codes:\n# 0 success\n# 1 build/output failure\n# 2 argument error\n\nset -euo pipefail\n\nusage() {\n cat \u003c\u003c'EOF'\nUsage:\n emit_rust_mir.sh --manifest \u003cCargo.toml> --out \u003cpath> [options] [-- \u003cextra cargo rustc args>]\n\nOptions:\n --manifest \u003cfile> Cargo manifest path (required)\n --out \u003cpath> Output .mir file or directory (required)\n --opt \u003cO0|O1|O2|O3> Opt level (default: O0)\n --crate \u003cpkg> Workspace package (-p)\n --bin \u003ctarget> Build only a specific bin target\n --lib Build only the lib target\n --help Show this help text\n\nExamples:\n emit_rust_mir.sh --manifest Cargo.toml --opt O0 --out /tmp/crate.O0.mir\n emit_rust_mir.sh --manifest Cargo.toml --out /tmp/zeroize_mir\nEOF\n}\n\ndie_arg() {\n echo \"emit_rust_mir.sh: $*\" >&2\n exit 2\n}\n\ndie_run() {\n echo \"emit_rust_mir.sh: $*\" >&2\n exit 1\n}\n\nrequire_value() {\n local opt=\"$1\"\n local val=\"${2-}\"\n [[ -n \"$val\" ]] || die_arg \"missing value for ${opt}\"\n}\n\nMANIFEST=\"\"\nOUT=\"\"\nOPT=\"O0\"\nCRATE=\"\"\nBIN_TARGET=\"\"\nLIB_TARGET=false\nEXTRA_ARGS=()\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --manifest)\n require_value \"$1\" \"${2-}\"\n MANIFEST=\"$2\"\n shift 2\n ;;\n --out)\n require_value \"$1\" \"${2-}\"\n OUT=\"$2\"\n shift 2\n ;;\n --opt)\n require_value \"$1\" \"${2-}\"\n OPT=\"$2\"\n shift 2\n ;;\n --crate)\n require_value \"$1\" \"${2-}\"\n CRATE=\"$2\"\n shift 2\n ;;\n --bin)\n require_value \"$1\" \"${2-}\"\n BIN_TARGET=\"$2\"\n shift 2\n ;;\n --lib)\n LIB_TARGET=true\n shift\n ;;\n --help | -h)\n usage\n exit 0\n ;;\n --)\n shift\n EXTRA_ARGS=(\"$@\")\n break\n ;;\n *)\n die_arg \"unknown argument: $1\"\n ;;\n esac\ndone\n\n[[ -n \"$MANIFEST\" ]] || die_arg \"--manifest is required\"\n[[ -n \"$OUT\" ]] || die_arg \"--out is required\"\n[[ -f \"$MANIFEST\" ]] || die_run \"manifest not found: $MANIFEST\"\n[[ -n \"$BIN_TARGET\" && \"$LIB_TARGET\" == true ]] && die_arg \"--bin and --lib are mutually exclusive\"\n\ncase \"$OPT\" in\n O0) LEVEL=\"0\" ;;\n O1) LEVEL=\"1\" ;;\n O2) LEVEL=\"2\" ;;\n O3) LEVEL=\"3\" ;;\n *) die_arg \"unsupported opt level: $OPT (use O0, O1, O2, O3)\" ;;\nesac\n\nOUT_IS_FILE=false\nif [[ \"$OUT\" == *.mir ]]; then\n OUT_IS_FILE=true\n mkdir -p \"$(dirname \"$OUT\")\"\nelse\n mkdir -p \"$OUT\"\nfi\n\nCARGO_ARGS=(+nightly rustc --manifest-path \"$MANIFEST\")\n[[ -n \"$CRATE\" ]] && CARGO_ARGS+=(\"-p\" \"$CRATE\")\n[[ -n \"$BIN_TARGET\" ]] && CARGO_ARGS+=(\"--bin\" \"$BIN_TARGET\")\n[[ \"$LIB_TARGET\" == true ]] && CARGO_ARGS+=(\"--lib\")\n\nTARGET_DIR=\"${TMPDIR:-/tmp}/zeroize_rust_mir_${LEVEL}_$\"\nrm -rf \"$TARGET_DIR\"\nmkdir -p \"$TARGET_DIR\"\n\necho \"=== emit_rust_mir.sh ===\"\necho \"manifest: $MANIFEST\"\necho \"opt: $OPT\"\necho \"target: $TARGET_DIR\"\necho \"output: $OUT\"\n\nif ! CARGO_TARGET_DIR=\"$TARGET_DIR\" cargo \"${CARGO_ARGS[@]}\" \\\n \"${EXTRA_ARGS[@]+\"${EXTRA_ARGS[@]}\"}\" \\\n -- --emit=mir -C opt-level=\"$LEVEL\"; then\n die_run \"cargo rustc failed for opt=${OPT}\"\nfi\n\ndeclare -a MIR_FILES=()\nwhile IFS= read -r file; do\n MIR_FILES+=(\"$file\")\ndone \u003c \u003c(find \"$TARGET_DIR\" -type f -name \"*.mir\" | LC_ALL=C sort)\n\n[[ \"${#MIR_FILES[@]}\" -gt 0 ]] || die_run \"no .mir files found under $TARGET_DIR\"\n\nif [[ \"$OUT_IS_FILE\" == true ]]; then\n : >\"$OUT\"\n for file in \"${MIR_FILES[@]}\"; do\n cat \"$file\" >>\"$OUT\"\n done\n [[ -s \"$OUT\" ]] || die_run \"emitted MIR is empty: $OUT\"\nelse\n cp \"${MIR_FILES[@]}\" \"$OUT/\"\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3536,"content_sha256":"95e3588f4484c6a872363a44e16587a51f5ba47cb05e6047edd7c598c6aa192d"},{"filename":"tools/extract_compile_flags.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\nExtract per-TU compilation flags from compile_commands.json.\n\nReads the compile database for a given source file and emits the compilation\nflags suitable for single-file LLVM IR or assembly emission via clang. Output\nand dependency-generation flags are stripped.\n\nUsage:\n python extract_compile_flags.py \\\\\n --compile-db compile_commands.json \\\\\n --src path/to/file.c \\\\\n [--format shell|json|lines] \\\\\n [--working-dir /override/cwd]\n\n # Recommended: capture as a bash array (works in both bash and zsh):\n FLAGS=()\n while IFS= read -r flag; do FLAGS+=(\"$flag\"); done \u003c \u003c(\n python {baseDir}/tools/extract_compile_flags.py \\\\\n --compile-db build/compile_commands.json \\\\\n --src src/crypto.c --format lines)\n {baseDir}/tools/emit_ir.sh --src src/crypto.c --out /tmp/out.ll --opt O2 -- \"${FLAGS[@]}\"\n\n # Get as JSON list:\n python {baseDir}/tools/extract_compile_flags.py \\\\\n --compile-db build/compile_commands.json \\\\\n --src src/crypto.c \\\\\n --format json\n\nExit codes:\n 0 flags written to stdout\n 1 compile_commands.json not found or contains invalid JSON\n 2 source file not found in the compile database\n\"\"\"\n\nimport argparse\nimport contextlib\nimport json\nimport re\nimport shlex\nimport sys\nfrom pathlib import Path\n\n# ---------------------------------------------------------------------------\n# Flags to strip: irrelevant or harmful for single-file IR/ASM emission.\n# Ordering matters for the \"takes an argument\" set — we must skip the next\n# token too.\n# ---------------------------------------------------------------------------\n\n# Flags that consume the next token as their argument and should be stripped.\n_STRIP_WITH_ARG = frozenset([\"-o\", \"-MF\", \"-MT\", \"-MQ\"])\n\n# Single-token flags to strip (no argument consumed).\n_STRIP_STANDALONE = frozenset(\n [\n \"-c\",\n \"-MD\",\n \"-MMD\",\n \"-MP\",\n \"-MG\",\n \"-pipe\",\n \"-save-temps\",\n \"-gsplit-dwarf\",\n ]\n)\n\n# Prefix patterns: strip any flag whose string starts with one of these.\n_STRIP_PREFIXES = (\n \"-fcrash-diagnostics-dir\",\n \"-fmodule-file=\",\n \"-fmodules-cache-path=\",\n \"-fpch-preprocess\",\n \"--serialize-diagnostics\",\n \"-fdebug-prefix-map=\",\n \"--debug-prefix-map=\",\n \"-iprefix\",\n \"-iwithprefix\",\n \"-iwithprefixbefore\",\n \"-fprofile-generate\",\n \"-fprofile-use=\",\n \"-fprofile-instr-generate\",\n \"-fprofile-instr-use=\",\n \"-fcoverage-mapping\",\n)\n\n# Regex for \"attached\" forms of strip-with-arg flags, e.g. \"-MFdepfile\" or \"-MF=depfile\".\n# These are single tokens that begin with one of the strip-with-arg prefixes.\n_STRIP_ATTACHED_RE = re.compile(r\"^(?:-o|-MF|-MT|-MQ)(?:=?.+)$\")\n\n\ndef _should_strip(flag: str) -> bool:\n \"\"\"Return True if this flag token should be removed from the output.\"\"\"\n if flag in _STRIP_STANDALONE:\n return True\n if _STRIP_ATTACHED_RE.match(flag):\n return True\n return any(flag.startswith(prefix) for prefix in _STRIP_PREFIXES)\n\n\ndef _extract_flags(raw_flags: list[str]) -> list[str]:\n \"\"\"\n Filter a list of raw flag tokens (excluding the compiler executable at index 0\n and the source file argument) down to the build-relevant subset.\n \"\"\"\n result: list[str] = []\n skip_next = False\n\n for token in raw_flags:\n if skip_next:\n skip_next = False\n continue\n\n # Strip-with-arg: consume this token and the next.\n if token in _STRIP_WITH_ARG:\n skip_next = True\n continue\n\n # Other strip conditions (standalone and prefixed).\n if _should_strip(token):\n continue\n\n result.append(token)\n\n return result\n\n\ndef _parse_command_string(command: str) -> list[str]:\n \"\"\"Split a shell command string into tokens using POSIX shlex rules.\"\"\"\n try:\n return shlex.split(command)\n except ValueError as exc:\n # Malformed quoting — best-effort split on whitespace.\n sys.stderr.write(f\"Warning: shlex.split failed ({exc}), falling back to whitespace split\\n\")\n return command.split()\n\n\ndef _normalize_path(path_str: str, directory: str) -> Path:\n \"\"\"Resolve a (possibly relative) path against a directory to an absolute Path.\"\"\"\n p = Path(path_str)\n if not p.is_absolute():\n p = Path(directory) / p\n return p.resolve()\n\n\ndef find_entry(db: list, src: str, working_dir: str | None = None) -> dict | None:\n \"\"\"\n Find the compile_commands.json entry for the given source file.\n\n Matching is done by resolving both the entry's 'file' field and the\n requested 'src' to absolute paths and comparing them. The first match\n is returned (some projects emit duplicates for different configurations).\n \"\"\"\n src_path = Path(src)\n if working_dir and not src_path.is_absolute():\n src_path = Path(working_dir) / src_path\n with contextlib.suppress(OSError):\n src_path = src_path.resolve() # file may not exist on disk; compare string form\n\n for entry in db:\n entry_dir = entry.get(\"directory\", \"\")\n entry_file = entry.get(\"file\", \"\")\n try:\n entry_path = _normalize_path(entry_file, entry_dir)\n except OSError:\n entry_path = Path(entry_file)\n\n if entry_path == src_path:\n return entry\n\n # Second pass: basename comparison (handles minor path discrepancies).\n src_basename = src_path.name\n for entry in db:\n entry_file = entry.get(\"file\", \"\")\n if Path(entry_file).name == src_basename:\n return entry\n\n return None\n\n\ndef get_raw_flags(entry: dict) -> list[str]:\n \"\"\"\n Extract the raw flag tokens from a compile_commands.json entry.\n\n Returns all tokens except the compiler executable (index 0) and the\n source file argument. The caller is responsible for further filtering.\n \"\"\"\n arguments: list[str] | None = entry.get(\"arguments\")\n if arguments is None:\n command = entry.get(\"command\", \"\")\n arguments = _parse_command_string(command)\n\n if not arguments:\n return []\n\n # Drop compiler executable (index 0) and the source file token.\n src_file = entry.get(\"file\", \"\")\n raw: list[str] = []\n for token in arguments[1:]:\n # Skip the source file itself (it will be specified via --src to emit_ir.sh).\n if token == src_file or (src_file and Path(token).name == Path(src_file).name):\n continue\n raw.append(token)\n\n return raw\n\n\ndef main() -> None:\n parser = argparse.ArgumentParser(\n description=\"Extract per-TU compile flags from compile_commands.json.\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=__doc__,\n )\n parser.add_argument(\n \"--compile-db\",\n required=True,\n metavar=\"PATH\",\n help=\"Path to compile_commands.json\",\n )\n parser.add_argument(\n \"--src\",\n required=True,\n metavar=\"FILE\",\n help=\"Source file to look up in the compile database\",\n )\n parser.add_argument(\n \"--format\",\n choices=[\"shell\", \"json\", \"lines\"],\n default=\"shell\",\n help=(\n \"Output format: 'shell' (space-separated, shell-quoted), 'json' list, \"\n \"or 'lines' (one flag per line, for array consumption) (default: shell)\"\n ),\n )\n parser.add_argument(\n \"--working-dir\",\n metavar=\"DIR\",\n default=None,\n help=\"Working directory for resolving relative --src paths (default: cwd)\",\n )\n args = parser.parse_args()\n\n # Load compile database.\n db_path = Path(args.compile_db)\n if not db_path.exists():\n sys.stderr.write(f\"Error: compile database not found: {db_path}\\n\")\n sys.exit(1)\n\n try:\n db = json.loads(db_path.read_text())\n except json.JSONDecodeError as exc:\n sys.stderr.write(f\"Error: invalid JSON in {db_path}: {exc}\\n\")\n sys.exit(1)\n\n if not isinstance(db, list):\n sys.stderr.write(f\"Error: expected a JSON array in {db_path}\\n\")\n sys.exit(1)\n\n # Find the entry for the requested source file.\n entry = find_entry(db, args.src, args.working_dir)\n if entry is None:\n sys.stderr.write(f\"Error: '{args.src}' not found in {db_path} ({len(db)} entries)\\n\")\n sys.exit(2)\n\n # Extract and filter flags.\n raw = get_raw_flags(entry)\n flags = _extract_flags(raw)\n\n # Output.\n if args.format == \"json\":\n print(json.dumps(flags))\n elif args.format == \"lines\":\n for f in flags:\n print(f)\n else:\n # Shell format: space-join of individually shell-quoted tokens.\n print(\" \".join(shlex.quote(f) for f in flags))\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8782,"content_sha256":"deb84d34ad07d77d6d16e63eee4ae6bfe492fa8a1893ca9d619dffd7dfee2c65"},{"filename":"tools/generate_poc.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.9\"\n# dependencies = [\"pyyaml>=6.0\"]\n# ///\n\"\"\"\nGenerate proof-of-concept C programs from zeroize-audit findings.\n\nEach PoC demonstrates that a finding is exploitable by reading sensitive\ndata that should have been zeroized. PoCs exit 0 when the secret persists\n(exploitable) and exit 1 when the data has been wiped (not exploitable).\n\nUsage:\n python generate_poc.py \\\\\n --findings \u003cfindings.json> \\\\\n --compile-db \u003ccompile_commands.json> \\\\\n --out \u003coutput_dir> \\\\\n [--categories CAT1,CAT2,...] \\\\\n [--config \u003cconfig.yaml>]\n\nExit codes:\n 0 PoCs generated successfully\n 1 Invalid input (bad JSON, missing required fields)\n 2 No exploitable findings in the selected categories\n 3 Output directory error\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport textwrap\nfrom pathlib import Path\nfrom typing import Any\n\ntry:\n import yaml\nexcept ImportError:\n yaml = None # type: ignore[assignment]\n\n# ---------------------------------------------------------------------------\n# Categories that support PoC generation\n# ---------------------------------------------------------------------------\nEXPLOITABLE_CATEGORIES = frozenset(\n [\n \"MISSING_SOURCE_ZEROIZE\",\n \"OPTIMIZED_AWAY_ZEROIZE\",\n \"STACK_RETENTION\",\n \"REGISTER_SPILL\",\n \"SECRET_COPY\",\n \"MISSING_ON_ERROR_PATH\",\n \"PARTIAL_WIPE\",\n \"NOT_ON_ALL_PATHS\",\n \"INSECURE_HEAP_ALLOC\",\n \"LOOP_UNROLLED_INCOMPLETE\",\n \"NOT_DOMINATING_EXITS\",\n ]\n)\n\n# ---------------------------------------------------------------------------\n# Defaults\n# ---------------------------------------------------------------------------\n_DEFAULT_SECRET_FILL: int = 0xAA\n_DEFAULT_SOURCE_INCLUSION_THRESHOLD: int = 5000\n_DEFAULT_STACK_PROBE_MAX: int = 4096\n_DEFAULT_MIN_CONFIDENCE: str = \"likely\"\n\n_CONFIDENCE_ORDER = {\"confirmed\": 0, \"likely\": 1, \"needs_review\": 2}\n\n_TOOLS_DIR = Path(__file__).resolve().parent\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _load_config(config_path: str | None) -> dict[str, Any]:\n \"\"\"Load a YAML config file and return the poc_generation section.\"\"\"\n if not config_path:\n return {}\n path = Path(config_path)\n if yaml is None:\n sys.stderr.write(\"Error: --config requires pyyaml. Install with: pip install pyyaml\\n\")\n sys.exit(1)\n if not path.exists():\n sys.stderr.write(f\"Error: config file not found: {path}\\n\")\n sys.exit(1)\n with open(path) as f:\n data = yaml.safe_load(f) or {}\n return data.get(\"poc_generation\", {})\n\n\ndef _get_compile_flags(compile_db: str, src_file: str) -> list[str] | None:\n \"\"\"Call extract_compile_flags.py and return flags as a list, or None on failure.\"\"\"\n script = _TOOLS_DIR / \"extract_compile_flags.py\"\n if not script.exists():\n sys.stderr.write(f\"Warning: compile flag extractor not found: {script}\\n\")\n return None\n try:\n result = subprocess.run(\n [\n sys.executable,\n str(script),\n \"--compile-db\",\n compile_db,\n \"--src\",\n src_file,\n \"--format\",\n \"json\",\n ],\n capture_output=True,\n text=True,\n timeout=30,\n )\n if result.returncode != 0:\n sys.stderr.write(\n f\"Warning: extract_compile_flags.py exited with code {result.returncode}\"\n f\" for {src_file}\\n\"\n )\n return None\n return json.loads(result.stdout)\n except subprocess.TimeoutExpired:\n sys.stderr.write(f\"Warning: extract_compile_flags.py timed out for {src_file}\\n\")\n return None\n except json.JSONDecodeError as exc:\n sys.stderr.write(\n f\"Warning: extract_compile_flags.py returned invalid JSON for {src_file}: {exc}\\n\"\n )\n return None\n except OSError as exc:\n sys.stderr.write(f\"Warning: failed to run extract_compile_flags.py for {src_file}: {exc}\\n\")\n return None\n\n\ndef _count_lines(path: str) -> int:\n \"\"\"Return the number of lines in a file, or 0 if unreadable.\"\"\"\n try:\n with open(path) as f:\n return sum(1 for _ in f)\n except OSError:\n return 0\n\n\ndef _extract_function_signature(src_file: str, line: int) -> str | None:\n \"\"\"\n Attempt to extract the function signature surrounding the given line number.\n Returns the function name if found, or None.\n \"\"\"\n try:\n with open(src_file) as f:\n lines = f.readlines()\n except OSError:\n return None\n\n # Search backwards from the finding line to find a function definition\n start = max(0, line - 30)\n end = min(len(lines), line + 5)\n region = \"\".join(lines[start:end])\n\n # Match C/C++ function definitions: return_type func_name(params) {\n pattern = re.compile(\n r\"(?:^|\\n)\\s*\"\n r\"(?:static\\s+|inline\\s+|extern\\s+|__attribute__\\s*\\([^)]*\\)\\s+)*\"\n r\"(?:(?:const\\s+|unsigned\\s+|signed\\s+|volatile\\s+)*\\w[\\w\\s*&]*?)\\s+\"\n r\"(\\w+)\\s*\\([^)]*\\)\\s*(?:\\{|$)\",\n re.MULTILINE,\n )\n matches = list(pattern.finditer(region))\n if matches:\n return matches[-1].group(1)\n return None\n\n\ndef _is_cpp_file(src_file: str) -> bool:\n \"\"\"Return True if the source file appears to be C++.\"\"\"\n ext = Path(src_file).suffix.lower()\n return ext in (\".cpp\", \".cxx\", \".cc\", \".C\", \".hpp\", \".hxx\")\n\n\ndef _is_rust_file(src_file: str) -> bool:\n \"\"\"Return True if the source file appears to be Rust.\"\"\"\n return Path(src_file).suffix.lower() == \".rs\"\n\n\ndef _relative_source_path(src_file: str, out_dir: str) -> str:\n \"\"\"Compute a relative path from out_dir to src_file.\"\"\"\n try:\n return os.path.relpath(src_file, out_dir)\n except ValueError:\n return src_file\n\n\n# ---------------------------------------------------------------------------\n# poc_common.h generation\n# ---------------------------------------------------------------------------\n\n\ndef _generate_common_header(\n secret_fill: int = _DEFAULT_SECRET_FILL, stack_probe_max: int = _DEFAULT_STACK_PROBE_MAX\n) -> str:\n return textwrap.dedent(f\"\"\"\\\n #ifndef POC_COMMON_H\n #define POC_COMMON_H\n\n #include \u003cstdio.h>\n #include \u003cstdlib.h>\n #include \u003cstring.h>\n #include \u003cstdint.h>\n\n #define SECRET_FILL_BYTE 0x{secret_fill:02X}\n #define STACK_PROBE_MAX {stack_probe_max}\n\n #define POC_PASS() do {{ \\\\\n fprintf(stderr, \"POC PASS: secret persists (exploitable)\\\\n\"); \\\\\n exit(0); \\\\\n }} while (0)\n\n #define POC_FAIL() do {{ \\\\\n fprintf(stderr, \"POC FAIL: secret was wiped (not exploitable)\\\\n\"); \\\\\n exit(1); \\\\\n }} while (0)\n\n /* Read through a volatile pointer to prevent the compiler from\n optimizing away the verification read. Returns non-zero if any\n byte in [ptr, ptr+len) is non-zero. */\n static int volatile_read_nonzero(const void *ptr, size_t len) {{\n const volatile unsigned char *p = (const volatile unsigned char *)ptr;\n int found = 0;\n for (size_t i = 0; i \u003c len; i++) {{\n if (p[i] != 0) {{\n found = 1;\n }}\n }}\n return found;\n }}\n\n /* Read through volatile pointer checking for the secret fill pattern. */\n static int volatile_read_has_secret(const void *ptr, size_t len) {{\n const volatile unsigned char *p = (const volatile unsigned char *)ptr;\n int count = 0;\n for (size_t i = 0; i \u003c len; i++) {{\n if (p[i] == SECRET_FILL_BYTE) {{\n count++;\n }}\n }}\n /* Consider it a match if >= 50% of bytes are the fill pattern */\n return count >= (int)(len / 2);\n }}\n\n /* Dump hex to stderr for diagnostics. */\n static void hex_dump(const char *label, const void *ptr, size_t len) {{\n const unsigned char *p = (const unsigned char *)ptr;\n fprintf(stderr, \"%s (%zu bytes):\", label, len);\n for (size_t i = 0; i \u003c len && i \u003c 64; i++) {{\n if (i % 16 == 0) fprintf(stderr, \"\\\\n \");\n fprintf(stderr, \"%02x \", p[i]);\n }}\n if (len > 64) fprintf(stderr, \"\\\\n ... (%zu more bytes)\", len - 64);\n fprintf(stderr, \"\\\\n\");\n }}\n\n /* Probe the stack for residual secret data from a prior call frame.\n Must be __attribute__((noinline, noclone)) so the compiler cannot\n merge this frame with the caller. */\n __attribute__((noinline))\n #if defined(__GNUC__) && !defined(__clang__)\n __attribute__((noclone))\n #endif\n static int stack_probe(size_t frame_size) {{\n if (frame_size > STACK_PROBE_MAX) frame_size = STACK_PROBE_MAX;\n volatile unsigned char probe[STACK_PROBE_MAX];\n /* Do NOT initialize — we want to read whatever is on the stack */\n int count = 0;\n for (size_t i = 0; i \u003c frame_size; i++) {{\n if (probe[i] == SECRET_FILL_BYTE) {{\n count++;\n }}\n }}\n return count >= (int)(frame_size / 4); /* 25% threshold */\n }}\n\n /* Fill a buffer with the secret marker pattern. */\n static void fill_secret(void *buf, size_t len) {{\n memset(buf, SECRET_FILL_BYTE, len);\n }}\n\n /* Check whether heap memory retains secret data after free+realloc.\n Do NOT compile with ASan — it poisons freed memory and hides the bug. */\n static int heap_residue_check(size_t alloc_size) {{\n void *ptr = malloc(alloc_size);\n if (!ptr) return 0;\n fill_secret(ptr, alloc_size);\n free(ptr);\n void *ptr2 = malloc(alloc_size);\n if (!ptr2) return 0;\n int found = volatile_read_has_secret(ptr2, alloc_size);\n hex_dump(\"Heap residue after free+realloc\", ptr2,\n alloc_size > 64 ? 64 : alloc_size);\n free(ptr2);\n return found;\n }}\n\n #endif /* POC_COMMON_H */\n \"\"\")\n\n\n# ---------------------------------------------------------------------------\n# Per-category PoC generators\n# ---------------------------------------------------------------------------\n\n\nclass PoCGenerator:\n \"\"\"Base class for per-category PoC generators.\"\"\"\n\n category: str = \"\"\n opt_level: str = \"-O0\"\n\n def __init__(\n self, finding: dict[str, Any], compile_db: str, out_dir: str, config: dict[str, Any]\n ):\n self.finding = finding\n self.compile_db = compile_db\n self.out_dir = out_dir\n self.config = config\n self.finding_id = finding.get(\"id\", \"unknown\")\n self.src_file = finding.get(\"file\", \"\")\n self.line = finding.get(\"line\", 0)\n self.symbol = finding.get(\"symbol\")\n self.requires_manual = False\n self.adjustment_notes: str | None = None\n\n def _func_name(self) -> str | None:\n if self.symbol:\n return self.symbol\n return _extract_function_signature(self.src_file, self.line)\n\n def _source_include_path(self) -> str:\n return _relative_source_path(self.src_file, self.out_dir)\n\n def _use_source_inclusion(self) -> bool:\n threshold = self.config.get(\n \"source_inclusion_threshold\", _DEFAULT_SOURCE_INCLUSION_THRESHOLD\n )\n return _count_lines(self.src_file) \u003c= threshold\n\n def _flags_str(self) -> str:\n flags = _get_compile_flags(self.compile_db, self.src_file)\n if flags is None:\n return \"\"\n # Filter out optimization flags — we set our own\n return \" \".join(f for f in flags if not re.match(r\"^-O[0-3sg]$\", f))\n\n def _poc_filename(self) -> str:\n safe_id = re.sub(r\"[^a-zA-Z0-9_-]\", \"_\", self.finding_id)\n ext = \".cpp\" if _is_cpp_file(self.src_file) else \".c\"\n return f\"poc_{safe_id}_{self.category.lower()}{ext}\"\n\n def _compiler_var(self) -> str:\n return \"$(CXX)\" if _is_cpp_file(self.src_file) else \"$(CC)\"\n\n def _include_directive(self) -> str:\n func = self._func_name()\n if self._use_source_inclusion():\n return f'#include \"{self._source_include_path()}\"'\n return f\"/* Link against object file containing {func or 'target function'} */\"\n\n def _build_poc_source(self, comment_lines: list[str], body_lines: list[str]) -> str:\n \"\"\"Assemble a PoC C source file with correct indentation.\"\"\"\n parts: list[str] = []\n parts.append(\"/* \" + comment_lines[0])\n for cl in comment_lines[1:]:\n parts.append(\" * \" + cl)\n parts.append(\" */\")\n parts.append('#include \"poc_common.h\"')\n parts.append(self._include_directive())\n parts.append(\"\")\n parts.append(\"int main(void) {\")\n for bl in body_lines:\n if bl == \"\":\n parts.append(\"\")\n else:\n parts.append(\" \" + bl)\n parts.append(\"}\")\n parts.append(\"\")\n return \"\\n\".join(parts)\n\n def generate(self) -> tuple[str, str]:\n \"\"\"Generate PoC source code. Returns (filename, source_code).\"\"\"\n raise NotImplementedError\n\n def makefile_target(self, filename: str) -> str:\n \"\"\"Return a Makefile target string for this PoC.\"\"\"\n binary = Path(filename).stem\n flags = self._flags_str()\n compiler = self._compiler_var()\n return (\n f\"{binary}: {filename} poc_common.h\\n\\t{compiler} {self.opt_level} {flags} -o $@ $\u003c\\n\"\n )\n\n def manifest_entry(self, filename: str) -> dict[str, Any]:\n \"\"\"Return a manifest entry for this PoC.\"\"\"\n entry: dict[str, Any] = {\n \"finding_id\": self.finding_id,\n \"category\": self.category,\n \"file\": filename,\n \"makefile_target\": Path(filename).stem,\n \"compile_opt\": self.opt_level,\n \"requires_manual_adjustment\": self.requires_manual,\n }\n if self.adjustment_notes:\n entry[\"adjustment_notes\"] = self.adjustment_notes\n return entry\n\n\nclass MissingSourceZeroizePoC(PoCGenerator):\n category = \"MISSING_SOURCE_ZEROIZE\"\n opt_level = \"-O0\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Call function at -O0, volatile-read buffer after return,\",\n \" verify secret persists.\",\n ]\n\n if func:\n body = [\n \"unsigned char secret_buf[256];\",\n \"fill_secret(secret_buf, sizeof(secret_buf));\",\n \"\",\n \"/* Call the function that handles the secret */\",\n f\"{func}(/* TODO: fill in arguments */);\",\n \"\",\n \"/* Check if the secret buffer still contains data */\",\n \"if (volatile_read_nonzero(secret_buf, sizeof(secret_buf)))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in arguments for {func}() call and adjust \"\n \"secret_buf to point to the actual sensitive variable.\"\n )\n else:\n body = [\n \"/* TODO: call the function that handles the secret */\",\n \"/* TODO: volatile-read the secret buffer after return */\",\n \"/* if (volatile_read_nonzero(ptr, len)) POC_PASS(); else POC_FAIL(); */\",\n 'fprintf(stderr, \"PoC requires manual adjustment\\\\n\");',\n \"exit(1);\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n \"Could not determine function signature. \"\n \"Fill in function call and secret buffer check.\"\n )\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass OptimizedAwayZeroizePoC(PoCGenerator):\n category = \"OPTIMIZED_AWAY_ZEROIZE\"\n\n def __init__(self, *args: Any, **kwargs: Any):\n super().__init__(*args, **kwargs)\n compiler_ev = self.finding.get(\"compiler_evidence\", {}) or {}\n diff_summary = compiler_ev.get(\"diff_summary\", \"\")\n match = re.search(r\"O([1-3s])\", diff_summary)\n if match:\n self.opt_level = f\"-O{match.group(1)}\"\n else:\n self.opt_level = \"-O2\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n f\"Strategy: Compile at {self.opt_level} where the wipe vanishes,\",\n \" call function, volatile-read buffer.\",\n ]\n\n if func:\n body = [\n \"unsigned char secret_buf[256];\",\n \"fill_secret(secret_buf, sizeof(secret_buf));\",\n \"\",\n \"/* Call function that contains the wipe the compiler removes */\",\n f\"{func}(/* TODO: fill in arguments */);\",\n \"\",\n \"/* At this opt level the compiler has removed the wipe.\",\n \" Volatile-read the buffer to see if secret persists. */\",\n \"if (volatile_read_nonzero(secret_buf, sizeof(secret_buf)))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in arguments for {func}(). \"\n f\"Compile at {self.opt_level} where the wipe disappears.\"\n )\n else:\n body = [\n \"/* TODO: call function whose wipe is optimized away */\",\n 'fprintf(stderr, \"PoC requires manual adjustment\\\\n\");',\n \"exit(1);\",\n ]\n self.requires_manual = True\n self.adjustment_notes = \"Could not determine function signature.\"\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass StackRetentionPoC(PoCGenerator):\n category = \"STACK_RETENTION\"\n opt_level = \"-O2\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n evidence = self.finding.get(\"evidence\", \"\")\n frame_match = re.search(r\"(\\d+)\\s*bytes?\\s*(?:frame|stack|alloc)\", evidence)\n frame_size = frame_match.group(1) if frame_match else \"256\"\n\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Call function, immediately call stack_probe() with\",\n \" matching frame size to detect residual secrets.\",\n ]\n\n if func:\n body = [\n \"/* Call the function that leaves secrets on the stack */\",\n f\"{func}(/* TODO: fill in arguments */);\",\n \"\",\n \"/* Immediately probe the stack for residual secret data */\",\n f\"if (stack_probe({frame_size}))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in arguments for {func}(). \"\n f\"Frame size {frame_size} is estimated from evidence; adjust if needed.\"\n )\n else:\n body = [\n \"/* TODO: call the function that retains secrets on stack */\",\n f\"if (stack_probe({frame_size}))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = \"Could not determine function signature.\"\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass RegisterSpillPoC(PoCGenerator):\n category = \"REGISTER_SPILL\"\n opt_level = \"-O2\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n evidence = self.finding.get(\"evidence\", \"\")\n offset_match = re.search(r\"-(\\d+)\\(%[re][sb]p\\)\", evidence)\n spill_offset = offset_match.group(1) if offset_match else \"64\"\n\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Like stack retention but probe the specific spill\",\n \" offset region from ASM evidence.\",\n ]\n\n if func:\n body = [\n \"/* Call the function that spills secrets to stack */\",\n f\"{func}(/* TODO: fill in arguments */);\",\n \"\",\n \"/* Probe the specific spill offset region */\",\n f\"if (stack_probe({spill_offset}))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in arguments for {func}(). \"\n f\"Spill offset {spill_offset} from ASM evidence; adjust if needed.\"\n )\n else:\n body = [\n \"/* TODO: call the function that spills registers to stack */\",\n f\"if (stack_probe({spill_offset}))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = \"Could not determine function signature.\"\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass SecretCopyPoC(PoCGenerator):\n category = \"SECRET_COPY\"\n opt_level = \"-O0\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Call function at -O0, verify original may be wiped,\",\n \" volatile-read the copy destination.\",\n ]\n\n if func:\n body = [\n \"/* Call function; it copies the secret internally */\",\n f\"{func}(/* TODO: fill in arguments */);\",\n \"\",\n \"/* The original may be wiped, but the copy destination persists.\",\n \" TODO: point this at the actual copy destination buffer. */\",\n \"unsigned char *copy_dest = NULL; /* TODO: set to copy destination */\",\n \"if (copy_dest && volatile_read_has_secret(copy_dest, 256))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in arguments for {func}() and set copy_dest to \"\n \"point to the buffer where the secret is copied.\"\n )\n else:\n body = [\n \"/* TODO: call the function that copies the secret */\",\n \"/* TODO: volatile-read the copy destination after return */\",\n 'fprintf(stderr, \"PoC requires manual adjustment\\\\n\");',\n \"exit(1);\",\n ]\n self.requires_manual = True\n self.adjustment_notes = \"Could not determine function signature or copy destination.\"\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass MissingOnErrorPathPoC(PoCGenerator):\n category = \"MISSING_ON_ERROR_PATH\"\n opt_level = \"-O0\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Force the error path via controlled input,\",\n \" volatile-read buffer after error return.\",\n ]\n\n if func:\n body = [\n \"unsigned char secret_buf[256];\",\n \"fill_secret(secret_buf, sizeof(secret_buf));\",\n \"\",\n \"/* Force the error path via controlled input.\",\n \" TODO: set up inputs that trigger the error return. */\",\n f\"int ret = {func}(/* TODO: error-triggering arguments */);\",\n \"\",\n 'fprintf(stderr, \"Function returned: %d\\\\n\", ret);',\n 'hex_dump(\"Secret buffer after error return\", secret_buf,',\n \" sizeof(secret_buf));\",\n \"\",\n \"/* After error return the secret should have been wiped */\",\n \"if (volatile_read_has_secret(secret_buf, sizeof(secret_buf)))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in error-triggering arguments for {func}(). \"\n \"The error path must be taken to demonstrate missing cleanup.\"\n )\n else:\n body = [\n \"/* TODO: call function with error-triggering inputs */\",\n \"/* TODO: volatile-read buffer after error return */\",\n 'fprintf(stderr, \"PoC requires manual adjustment\\\\n\");',\n \"exit(1);\",\n ]\n self.requires_manual = True\n self.adjustment_notes = \"Could not determine function signature.\"\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass PartialWipePoC(PoCGenerator):\n category = \"PARTIAL_WIPE\"\n opt_level = \"-O0\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n evidence = self.finding.get(\"evidence\", \"\")\n\n # Try to extract wiped vs full sizes from evidence\n size_matches = re.findall(r\"(\\d+)\\s*bytes?\", evidence)\n if len(size_matches) >= 2:\n wiped_size = size_matches[0]\n full_size = size_matches[1]\n else:\n wiped_size = \"8\"\n full_size = \"256\"\n\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Fill full buffer with secret, call function, volatile-read\",\n \" the tail beyond the incorrectly-sized wipe.\",\n ]\n\n if func:\n body = [\n f\"unsigned char buf[{full_size}];\",\n f\"fill_secret(buf, {full_size});\",\n \"\",\n \"/* Call function that partially wipes the buffer */\",\n f\"{func}(/* TODO: fill in arguments */);\",\n \"\",\n f\"/* The wipe covers only {wiped_size} bytes of {full_size}.\",\n \" Check the tail beyond the wiped region. */\",\n f\"if (volatile_read_has_secret(buf + {wiped_size}, {full_size} - {wiped_size}))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in arguments for {func}(). \"\n f\"Wiped size {wiped_size} and full size {full_size} are estimated \"\n \"from evidence; adjust if needed.\"\n )\n else:\n body = [\n f\"unsigned char buf[{full_size}];\",\n f\"fill_secret(buf, {full_size});\",\n \"\",\n \"/* TODO: call the function that partially wipes the buffer */\",\n \"\",\n f\"/* Check tail beyond the {wiped_size}-byte wipe */\",\n f\"if (volatile_read_has_secret(buf + {wiped_size}, {full_size} - {wiped_size}))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n \"Could not determine function signature. \"\n f\"Wiped size {wiped_size} and full size {full_size} are estimated; \"\n \"adjust if needed.\"\n )\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass NotOnAllPathsPoC(PoCGenerator):\n category = \"NOT_ON_ALL_PATHS\"\n opt_level = \"-O0\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n evidence = self.finding.get(\"evidence\", \"\")\n\n # Try to extract uncovered path line from evidence\n line_match = re.search(r\"line (\\d+)\", evidence)\n uncovered_line = line_match.group(1) if line_match else \"unknown\"\n\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Force execution down the uncovered path that lacks the wipe,\",\n \" then volatile-read the secret buffer.\",\n ]\n\n if func:\n body = [\n \"unsigned char secret_buf[256];\",\n \"fill_secret(secret_buf, sizeof(secret_buf));\",\n \"\",\n \"/* Force the uncovered path (no wipe).\",\n f\" TODO: set up inputs that take the path at line {uncovered_line}. */\",\n f\"{func}(/* TODO: path-forcing arguments */);\",\n \"\",\n \"/* After taking the uncovered path the secret should persist */\",\n \"if (volatile_read_has_secret(secret_buf, sizeof(secret_buf)))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in arguments for {func}() that force execution through \"\n f\"the uncovered path (line {uncovered_line}). \"\n \"Identify which inputs bypass the wipe.\"\n )\n else:\n body = [\n \"/* TODO: call function with inputs that take the uncovered path */\",\n \"/* TODO: volatile-read buffer after return */\",\n 'fprintf(stderr, \"PoC requires manual adjustment\\\\n\");',\n \"exit(1);\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n \"Could not determine function signature. \"\n \"Identify inputs that force the uncovered path.\"\n )\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass InsecureHeapAllocPoC(PoCGenerator):\n category = \"INSECURE_HEAP_ALLOC\"\n opt_level = \"-O0\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n evidence = self.finding.get(\"evidence\", \"\")\n\n # Extract allocation size and allocator from evidence\n size_match = re.search(r\"(\\d+)\", evidence)\n alloc_size = size_match.group(1) if size_match else \"256\"\n alloc_match = re.search(r\"(malloc|calloc|realloc)\", evidence)\n allocator = alloc_match.group(1) if alloc_match else \"malloc\"\n\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Demonstrate heap residue — allocate, fill with secret, free,\",\n \" re-allocate same size, check if secret persists.\",\n \"NOTE: Do NOT compile with ASan (it poisons freed memory).\",\n ]\n\n body = [\n f\"/* Demonstrate that {allocator}() leaves secret residue after free */\",\n f\"if (heap_residue_check({alloc_size}))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n\n if func:\n body.extend(\n [\n \"\",\n \"/* Additionally, call the function that uses the insecure allocator\",\n \" and verify residue after it returns. */\",\n f\"/* {func}(/ * TODO: fill in arguments * /); */\",\n ]\n )\n self.requires_manual = False # Self-contained heap check works\n self.adjustment_notes = (\n f\"The self-contained heap_residue_check() demonstrates the \"\n f\"vulnerability. Optionally uncomment and fill in {func}() \"\n \"for a function-specific test.\"\n )\n else:\n self.requires_manual = False\n self.adjustment_notes = (\n f\"Self-contained PoC using heap_residue_check({alloc_size}). \"\n \"Optionally add a call to the target function for specificity.\"\n )\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass LoopUnrolledIncompletePoC(PoCGenerator):\n category = \"LOOP_UNROLLED_INCOMPLETE\"\n opt_level = \"-O2\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n evidence = self.finding.get(\"evidence\", \"\")\n\n # Extract covered bytes and object size from evidence\n covered_match = re.search(r\"(\\d+)\\s*consecutive\", evidence)\n covered_bytes = covered_match.group(1) if covered_match else \"16\"\n size_match = re.search(r\"object size is (\\d+)\", evidence)\n full_size = size_match.group(1) if size_match else \"256\"\n\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Compile at -O2 where incomplete loop unrolling occurs.\",\n f\" Fill buffer, call function, check tail beyond {covered_bytes}\",\n f\" unrolled bytes (object size: {full_size}).\",\n ]\n\n if func:\n body = [\n f\"unsigned char buf[{full_size}];\",\n f\"fill_secret(buf, {full_size});\",\n \"\",\n \"/* Call function whose wipe loop is incompletely unrolled at -O2 */\",\n f\"{func}(/* TODO: fill in arguments */);\",\n \"\",\n f\"/* The compiler unrolled {covered_bytes} bytes of the wipe loop\",\n f\" but the object is {full_size} bytes. Check the tail. */\",\n (\n f\"if (volatile_read_has_secret(buf + {covered_bytes},\"\n f\" {full_size} - {covered_bytes}))\"\n ),\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in arguments for {func}(). \"\n f\"Covered bytes {covered_bytes} and object size {full_size} are \"\n \"estimated from IR evidence; adjust if needed. \"\n \"Must compile at -O2 for unrolling to occur.\"\n )\n else:\n body = [\n f\"unsigned char buf[{full_size}];\",\n f\"fill_secret(buf, {full_size});\",\n \"\",\n \"/* TODO: call function with incompletely unrolled wipe loop */\",\n \"\",\n f\"/* Check tail beyond the {covered_bytes}-byte unrolled region */\",\n (\n f\"if (volatile_read_has_secret(buf + {covered_bytes},\"\n f\" {full_size} - {covered_bytes}))\"\n ),\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n \"Could not determine function signature. \"\n f\"Covered bytes {covered_bytes} and object size {full_size} are \"\n \"estimated; adjust if needed.\"\n )\n\n return filename, self._build_poc_source(comment, body)\n\n\nclass NotDominatingExitsPoC(PoCGenerator):\n category = \"NOT_DOMINATING_EXITS\"\n opt_level = \"-O0\"\n\n def generate(self) -> tuple[str, str]:\n func = self._func_name()\n filename = self._poc_filename()\n evidence = self.finding.get(\"evidence\", \"\")\n\n # Extract exit line or path count from CFG evidence\n exit_match = re.search(r\"exit at line (\\d+)\", evidence)\n path_match = re.search(r\"(\\d+) of (\\d+) exit paths\", evidence)\n if exit_match:\n exit_info = f\"line {exit_match.group(1)}\"\n elif path_match:\n exit_info = f\"{path_match.group(1)} of {path_match.group(2)} exit paths\"\n else:\n exit_info = \"an exit path that bypasses the wipe\"\n\n comment = [\n f\"PoC for finding {self.finding_id}: {self.category}\",\n f\"Source: {self.src_file}:{self.line}\",\n \"Strategy: Force execution through an exit path that bypasses the wipe\",\n f\" (CFG evidence: {exit_info}), then volatile-read the secret.\",\n ]\n\n if func:\n body = [\n \"unsigned char secret_buf[256];\",\n \"fill_secret(secret_buf, sizeof(secret_buf));\",\n \"\",\n \"/* Force execution through the exit path that bypasses the wipe.\",\n f\" CFG shows the wipe does not dominate {exit_info}.\",\n \" TODO: set up inputs that reach this exit path. */\",\n f\"{func}(/* TODO: exit-path-forcing arguments */);\",\n \"\",\n \"/* After taking the non-dominated exit the secret should persist */\",\n \"if (volatile_read_has_secret(secret_buf, sizeof(secret_buf)))\",\n \" POC_PASS();\",\n \"else\",\n \" POC_FAIL();\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n f\"Fill in arguments for {func}() that force execution through \"\n f\"{exit_info} (the exit not dominated by the wipe). \"\n \"Requires understanding of the function's control flow.\"\n )\n else:\n body = [\n \"/* TODO: call function with inputs that reach the non-dominated exit */\",\n \"/* TODO: volatile-read buffer after return */\",\n 'fprintf(stderr, \"PoC requires manual adjustment\\\\n\");',\n \"exit(1);\",\n ]\n self.requires_manual = True\n self.adjustment_notes = (\n \"Could not determine function signature. \"\n \"Identify inputs that reach the exit path bypassing the wipe.\"\n )\n\n return filename, self._build_poc_source(comment, body)\n\n\n# ---------------------------------------------------------------------------\n# Category -> generator mapping\n# ---------------------------------------------------------------------------\n_GENERATORS: dict[str, type] = {\n \"MISSING_SOURCE_ZEROIZE\": MissingSourceZeroizePoC,\n \"OPTIMIZED_AWAY_ZEROIZE\": OptimizedAwayZeroizePoC,\n \"STACK_RETENTION\": StackRetentionPoC,\n \"REGISTER_SPILL\": RegisterSpillPoC,\n \"SECRET_COPY\": SecretCopyPoC,\n \"MISSING_ON_ERROR_PATH\": MissingOnErrorPathPoC,\n \"PARTIAL_WIPE\": PartialWipePoC,\n \"NOT_ON_ALL_PATHS\": NotOnAllPathsPoC,\n \"INSECURE_HEAP_ALLOC\": InsecureHeapAllocPoC,\n \"LOOP_UNROLLED_INCOMPLETE\": LoopUnrolledIncompletePoC,\n \"NOT_DOMINATING_EXITS\": NotDominatingExitsPoC,\n}\n\n\n# ---------------------------------------------------------------------------\n# Makefile generation\n# ---------------------------------------------------------------------------\n\n\ndef _generate_makefile(targets: list[dict[str, str]]) -> str:\n \"\"\"Generate a Makefile for all PoC targets.\"\"\"\n lines = [\n \"# Auto-generated by generate_poc.py\",\n \"# Build: make all\",\n \"# Run: make run\",\n \"\",\n \"CC ?= cc\",\n \"CXX ?= c++\",\n \"CFLAGS ?= -Wall -Wextra\",\n \"CXXFLAGS ?= -Wall -Wextra\",\n \"\",\n \"BINARIES =\",\n ]\n\n binary_names = []\n target_blocks = []\n\n for t in targets:\n binary = t[\"binary\"]\n binary_names.append(binary)\n target_blocks.append(t[\"rule\"])\n\n lines[9] = \"BINARIES = \" + \" \".join(binary_names)\n lines.append(\"\")\n lines.append(\".PHONY: all run clean\")\n lines.append(\"\")\n lines.append(\"all: $(BINARIES)\")\n lines.append(\"\")\n\n # Run target\n lines.append(\"run: all\")\n for name in binary_names:\n lines.append(f\"\\t@echo '--- Running {name} ---'\")\n lines.append(f\"\\t@./{name} && echo 'RESULT: EXPLOITABLE' || echo 'RESULT: NOT EXPLOITABLE'\")\n lines.append(\"\")\n\n # Per-target rules\n for block in target_blocks:\n lines.append(block)\n lines.append(\"\")\n\n lines.append(\"clean:\")\n lines.append(\"\\trm -f $(BINARIES)\")\n lines.append(\"\")\n\n return \"\\n\".join(lines)\n\n\n# ---------------------------------------------------------------------------\n# Main logic\n# ---------------------------------------------------------------------------\n\n\ndef _filter_findings(\n findings: list[dict[str, Any]], categories: frozenset, min_confidence: str | None\n) -> list[dict[str, Any]]:\n \"\"\"Filter findings to only exploitable categories above confidence threshold.\n\n When min_confidence is None, all findings in the selected categories are\n returned regardless of confidence level.\n \"\"\"\n result = []\n for f in findings:\n cat = f.get(\"category\", \"\")\n if cat not in categories:\n continue\n if min_confidence is None:\n result.append(f)\n continue\n threshold = _CONFIDENCE_ORDER.get(min_confidence, 2)\n # Map needs_review boolean to confidence string\n conf = \"needs_review\" if f.get(\"needs_review\", False) else \"likely\"\n # Check evidence/compiler_evidence for confirmed signals\n if f.get(\"compiler_evidence\"):\n conf = \"confirmed\"\n # CFG-backed findings use evidence_source instead of compiler_evidence\n evidence_sources = f.get(\"evidence_source\", [])\n if isinstance(evidence_sources, list) and \"cfg\" in evidence_sources:\n conf = \"confirmed\"\n if _CONFIDENCE_ORDER.get(conf, 2) \u003c= threshold:\n result.append(f)\n return result\n\n\ndef run(\n findings_path: str,\n compile_db: str,\n out_dir: str,\n categories: list[str] | None = None,\n config_path: str | None = None,\n no_confidence_filter: bool = False,\n) -> int:\n \"\"\"Main entry point. Returns exit code.\n\n Args:\n no_confidence_filter: When True, generate PoCs for all findings\n regardless of confidence level.\n \"\"\"\n\n # Load findings\n try:\n with open(findings_path) as f:\n data = json.load(f)\n except (OSError, json.JSONDecodeError) as exc:\n sys.stderr.write(f\"Error: cannot read findings: {exc}\\n\")\n return 1\n\n # Support both top-level array and {findings: [...]} format\n if isinstance(data, list):\n findings = data\n elif isinstance(data, dict):\n findings = data.get(\"findings\", [])\n else:\n sys.stderr.write(\"Error: findings must be a JSON array or object with 'findings' key\\n\")\n return 1\n\n # Load config\n config = _load_config(config_path)\n min_confidence: str | None = (\n None if no_confidence_filter else config.get(\"min_confidence\", _DEFAULT_MIN_CONFIDENCE)\n )\n secret_fill = config.get(\"secret_fill_byte\", _DEFAULT_SECRET_FILL)\n stack_probe_max = config.get(\"stack_probe_max_size\", _DEFAULT_STACK_PROBE_MAX)\n\n # Determine categories\n if categories:\n selected = frozenset(categories) & EXPLOITABLE_CATEGORIES\n else:\n selected = EXPLOITABLE_CATEGORIES\n\n # Filter findings\n exploitable = _filter_findings(findings, selected, min_confidence)\n if not exploitable:\n sys.stderr.write(\"No exploitable findings found in selected categories.\\n\")\n return 2\n\n # Create output directory\n try:\n os.makedirs(out_dir, exist_ok=True)\n except OSError as exc:\n sys.stderr.write(f\"Error: cannot create output directory: {exc}\\n\")\n return 3\n\n # Write poc_common.h\n common_h = _generate_common_header(secret_fill, stack_probe_max)\n with open(os.path.join(out_dir, \"poc_common.h\"), \"w\") as f:\n f.write(common_h)\n\n # Generate PoCs\n makefile_targets: list[dict[str, str]] = []\n manifest_entries: list[dict[str, Any]] = []\n generated_count = 0\n manual_count = 0\n\n for finding in exploitable:\n cat = finding.get(\"category\", \"\")\n gen_cls = _GENERATORS.get(cat)\n if gen_cls is None:\n continue\n\n gen = gen_cls(finding, compile_db, out_dir, config)\n filename, source = gen.generate()\n\n # Write PoC source\n poc_path = os.path.join(out_dir, filename)\n with open(poc_path, \"w\") as f:\n f.write(source)\n\n # Collect Makefile target\n binary = Path(filename).stem\n makefile_targets.append(\n {\n \"binary\": binary,\n \"rule\": gen.makefile_target(filename),\n }\n )\n\n # Collect manifest entry\n manifest_entries.append(gen.manifest_entry(filename))\n\n generated_count += 1\n if gen.requires_manual:\n manual_count += 1\n\n # Write Makefile\n makefile_content = _generate_makefile(makefile_targets)\n with open(os.path.join(out_dir, \"Makefile\"), \"w\") as f:\n f.write(makefile_content)\n\n # Write manifest\n manifest = {\n \"pocs_generated\": generated_count,\n \"pocs_requiring_adjustment\": manual_count,\n \"output_dir\": out_dir,\n \"categories_covered\": sorted(set(e[\"category\"] for e in manifest_entries)),\n \"entries\": manifest_entries,\n }\n with open(os.path.join(out_dir, \"poc_manifest.json\"), \"w\") as f:\n json.dump(manifest, f, indent=2)\n f.write(\"\\n\")\n\n # Summary\n sys.stderr.write(\n f\"Generated {generated_count} PoC(s) in {out_dir}/ \"\n f\"({manual_count} requiring manual adjustment)\\n\"\n )\n return 0\n\n\ndef main() -> None:\n parser = argparse.ArgumentParser(\n description=\"Generate proof-of-concept programs from zeroize-audit findings.\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=__doc__,\n )\n parser.add_argument(\n \"--findings\",\n required=True,\n metavar=\"PATH\",\n help=\"Path to findings JSON (array or {findings: [...]})\",\n )\n parser.add_argument(\n \"--compile-db\",\n required=True,\n metavar=\"PATH\",\n help=\"Path to compile_commands.json\",\n )\n parser.add_argument(\n \"--out\",\n required=True,\n metavar=\"DIR\",\n help=\"Output directory for generated PoCs\",\n )\n parser.add_argument(\n \"--categories\",\n metavar=\"CAT1,CAT2,...\",\n default=None,\n help=\"Comma-separated list of finding categories (default: all exploitable)\",\n )\n parser.add_argument(\n \"--config\",\n metavar=\"PATH\",\n default=None,\n help=\"Path to config YAML with poc_generation section\",\n )\n parser.add_argument(\n \"--no-confidence-filter\",\n action=\"store_true\",\n default=False,\n help=\"Generate PoCs for all findings regardless of confidence level\",\n )\n args = parser.parse_args()\n\n categories = None\n if args.categories:\n categories = [c.strip() for c in args.categories.split(\",\")]\n\n sys.exit(\n run(\n args.findings,\n args.compile_db,\n args.out,\n categories=categories,\n config_path=args.config,\n no_confidence_filter=args.no_confidence_filter,\n )\n )\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":48412,"content_sha256":"d3b44351a526773a513034bad7f838dc61ceb4a1a1e77b737d72c15b7c820005"},{"filename":"tools/mcp/apply_confidence_gates.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\nApply strict confidence gates to zeroize-audit findings.\n\"\"\"\n\nimport argparse\nimport json\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nADVANCED_MCP_CATEGORIES = {\n \"SECRET_COPY\",\n \"MISSING_ON_ERROR_PATH\",\n \"NOT_DOMINATING_EXITS\",\n}\n\nASM_REQUIRED_CATEGORIES = {\n \"STACK_RETENTION\",\n \"REGISTER_SPILL\",\n}\n\n\ndef _has_compiler_evidence(finding: dict[str, Any]) -> bool:\n ce = finding.get(\"compiler_evidence\")\n if not isinstance(ce, dict):\n return False\n return any(ce.get(key) for key in (\"o0\", \"o2\", \"diff_summary\"))\n\n\ndef _has_marker(text: str, marker: str) -> bool:\n return marker in text.lower()\n\n\ndef apply_gates(\n report: dict[str, Any],\n mcp_available: bool,\n require_mcp_for_advanced: bool,\n) -> dict[str, Any]:\n findings: list[dict[str, Any]] = report.get(\"findings\", [])\n\n for finding in findings:\n category = finding.get(\"category\")\n evidence = (finding.get(\"evidence\") or \"\").lower()\n\n if category in {\"OPTIMIZED_AWAY_ZEROIZE\"} and not _has_compiler_evidence(finding):\n finding[\"needs_review\"] = True\n finding[\"evidence\"] = (\n finding.get(\"evidence\", \"\")\n + \" [gated: missing IR/ASM evidence for optimized-away claim]\"\n ).strip()\n\n if category in ASM_REQUIRED_CATEGORIES and not _has_marker(evidence, \"asm\"):\n finding[\"needs_review\"] = True\n finding[\"evidence\"] = (\n finding.get(\"evidence\", \"\") + \" [gated: missing assembly evidence]\"\n ).strip()\n\n if require_mcp_for_advanced and not mcp_available and category in ADVANCED_MCP_CATEGORIES:\n finding[\"needs_review\"] = True\n finding[\"evidence\"] = (\n finding.get(\"evidence\", \"\")\n + \" [gated: MCP unavailable for advanced semantic finding]\"\n ).strip()\n\n summary = report.get(\"summary\", {})\n if isinstance(summary, dict):\n summary[\"issues_found\"] = len(findings)\n\n return report\n\n\ndef main() -> None:\n parser = argparse.ArgumentParser(description=\"Apply zeroize-audit confidence gates\")\n parser.add_argument(\"--input\", required=True, help=\"Input output.json path\")\n parser.add_argument(\"--out\", required=True, help=\"Output path\")\n parser.add_argument(\n \"--mcp-available\",\n action=\"store_true\",\n help=\"Set when MCP semantic evidence is available\",\n )\n parser.add_argument(\n \"--require-mcp-for-advanced\",\n action=\"store_true\",\n help=\"Downgrade advanced findings when MCP is unavailable\",\n )\n args = parser.parse_args()\n\n report = json.loads(Path(args.input).read_text())\n if not isinstance(report, dict):\n print(\n f\"Error: expected JSON object in {args.input}, got {type(report).__name__}\",\n file=sys.stderr,\n )\n sys.exit(1)\n updated = apply_gates(\n report=report,\n mcp_available=args.mcp_available,\n require_mcp_for_advanced=args.require_mcp_for_advanced,\n )\n\n out_path = Path(args.out)\n out_path.parent.mkdir(parents=True, exist_ok=True)\n out_path.write_text(json.dumps(updated, indent=2) + \"\\n\")\n print(f\"OK: wrote gated report to {out_path}\")\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3374,"content_sha256":"e4b9f1e2df523614ca61c0c1a89cafad05f487056cfe40aafa6fef4546d5e2f8"},{"filename":"tools/mcp/check_mcp.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# Probe for Serena MCP server availability.\n#\n# Usage:\n# check_mcp.sh\n# check_mcp.sh --compile-db compile_commands.json\n\nusage() {\n echo \"Usage: $0 [--compile-db compile_commands.json]\" >&2\n}\n\nCOMPILE_DB=\"\"\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --compile-db)\n COMPILE_DB=\"$2\"\n shift 2\n ;;\n *)\n echo \"Unknown arg: $1\" >&2\n usage\n exit 2\n ;;\n esac\ndone\n\nmissing=()\nif ! command -v \"uvx\" >/dev/null 2>&1; then\n missing+=(\"uvx\")\nfi\n\ncompile_db_status=\"not_checked\"\nif [[ -n \"$COMPILE_DB\" ]]; then\n if [[ -f \"$COMPILE_DB\" ]]; then\n compile_db_status=\"present\"\n else\n compile_db_status=\"missing\"\n fi\nfi\n\nif [[ ${#missing[@]} -eq 0 ]]; then\n cat \u003c\u003cEOF\n{\n \"mcp_available\": true,\n \"mcp_server\": \"serena\",\n \"uvx_present\": true,\n \"compile_db_status\": \"${compile_db_status}\",\n \"missing_tools\": []\n}\nEOF\n exit 0\nfi\n\nmissing_json=$(printf '\"%s\",' \"${missing[@]}\" | sed 's/,$//')\n\ncat \u003c\u003cEOF\n{\n \"mcp_available\": false,\n \"mcp_server\": \"serena\",\n \"compile_db_status\": \"${compile_db_status}\",\n \"missing_tools\": [${missing_json}],\n \"message\": \"Serena MCP server unavailable (uvx not found); advanced findings must be downgraded to needs_review.\"\n}\nEOF\nexit 1\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1251,"content_sha256":"68c9291036b42551f926bb2bd0adeeb11e920232f2d09d4a22712575f1a9a3ae"},{"filename":"tools/mcp/normalize_mcp_evidence.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\nNormalize Serena MCP semantic-analysis output into consistent evidence records.\n\nSerena returns structured results with file, line, symbol, and kind fields.\nThis normalizer produces a consistent schema consumed by the zeroize-audit\nconfidence gating and evidence scoring pipeline.\n\"\"\"\n\nimport argparse\nimport json\nimport sys\nfrom collections import Counter\nfrom pathlib import Path\nfrom typing import Any\n\n\ndef _load_payload(input_path: str) -> Any:\n if input_path:\n try:\n return json.loads(Path(input_path).read_text(encoding=\"utf-8\"))\n except (OSError, json.JSONDecodeError) as e:\n print(f\"Error reading {input_path}: {e}\", file=sys.stderr)\n sys.exit(1)\n if sys.stdin.isatty():\n print(\"Error: no --input specified and stdin is a terminal\", file=sys.stderr)\n sys.exit(2)\n try:\n return json.load(sys.stdin)\n except json.JSONDecodeError as e:\n print(f\"Error: invalid JSON on stdin: {e}\", file=sys.stderr)\n sys.exit(1)\n\n\ndef _as_results(payload: Any) -> list[dict[str, Any]]:\n if isinstance(payload, list):\n return [item for item in payload if isinstance(item, dict)]\n if isinstance(payload, dict):\n if isinstance(payload.get(\"results\"), list):\n return [item for item in payload[\"results\"] if isinstance(item, dict)]\n return [payload]\n return []\n\n\ndef _normalize_item(result: dict[str, Any], item: dict[str, Any]) -> dict[str, Any]:\n file_path = item.get(\"file\") or item.get(\"uri\") or result.get(\"target\") or \"\"\n line = item.get(\"line\")\n if isinstance(line, str) and line.isdigit():\n line = int(line)\n\n symbol = item.get(\"symbol\") or item.get(\"name\") or result.get(\"query\") or \"\"\n kind = item.get(\"kind\") or result.get(\"tool\") or \"mcp_result\"\n detail = item.get(\"detail\") or item.get(\"snippet\") or \"\"\n\n confidence = item.get(\"confidence\") if item.get(\"confidence\") is not None else \"medium\"\n\n return {\n \"file\": file_path,\n \"line\": line,\n \"symbol\": symbol,\n \"kind\": kind,\n \"detail\": detail,\n \"source\": result.get(\"tool\", \"mcp\"),\n \"confidence\": confidence,\n \"metadata\": {\n \"query\": result.get(\"query\"),\n \"target\": result.get(\"target\"),\n \"raw_item\": item,\n },\n }\n\n\ndef normalize(payload: Any) -> dict[str, Any]:\n results = _as_results(payload)\n normalized: list[dict[str, Any]] = []\n tools = Counter()\n kinds = Counter()\n\n for result in results:\n tool_name = result.get(\"tool\", \"mcp\")\n tools[tool_name] += 1\n\n items = result.get(\"items\")\n if not isinstance(items, list):\n items = [result]\n\n for raw_item in items:\n if not isinstance(raw_item, dict):\n continue\n entry = _normalize_item(result, raw_item)\n normalized.append(entry)\n kinds[entry[\"kind\"]] += 1\n\n return {\n \"mcp_available\": len(normalized) > 0,\n \"evidence_count\": len(normalized),\n \"evidence\": normalized,\n \"coverage\": {\n \"by_tool\": dict(tools),\n \"by_kind\": dict(kinds),\n },\n }\n\n\ndef main() -> None:\n parser = argparse.ArgumentParser(description=\"Normalize MCP evidence JSON\")\n parser.add_argument(\"--input\", help=\"Input JSON file path; defaults to stdin\")\n parser.add_argument(\"--out\", required=True, help=\"Output JSON path\")\n args = parser.parse_args()\n\n payload = _load_payload(args.input)\n output = normalize(payload)\n\n out_path = Path(args.out)\n out_path.parent.mkdir(parents=True, exist_ok=True)\n out_path.write_text(json.dumps(output, indent=2) + \"\\n\", encoding=\"utf-8\")\n print(f\"OK: wrote normalized MCP evidence to {out_path}\")\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3906,"content_sha256":"5c7132a27f7ca1ac88728115273a78d6b11a8f87050d8262fa1c9dd47483b7eb"},{"filename":"tools/scripts/check_llvm_patterns.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\ncheck_llvm_patterns.py — LLVM IR comparison for Rust dead-store-elimination findings.\n\nReads LLVM IR files emitted by emit_rust_ir.sh (required: O0 and O2; optional: O1/O3) and detects:\n- Volatile store count drop O0→O2 (OPTIMIZED_AWAY_ZEROIZE)\n- Non-volatile llvm.memset on secret-sized range (OPTIMIZED_AWAY_ZEROIZE)\n- alloca with @llvm.lifetime.end but no store volatile (STACK_RETENTION)\n- Secret alloca present at O0 but absent at O2 (SROA/mem2reg) (OPTIMIZED_AWAY_ZEROIZE)\n- Secret value in argument registers at call site (REGISTER_SPILL)\n\nUsage:\n uv run check_llvm_patterns.py --o0 \u003cfile.O0.ll> --o2 \u003cfile.O2.ll> --out \u003cfindings.json>\n uv run check_llvm_patterns.py \\\n --o0 \u003cfile.O0.ll> --o1 \u003cfile.O1.ll> --o2 \u003cfile.O2.ll> --o3 \u003cfile.O3.ll> \\\n --out \u003cfindings.json>\n\nExit codes:\n 0 — ran successfully (findings may be empty)\n 1 — input file not found\n 2 — argument error\n\"\"\"\n\nimport argparse\nimport json\nimport re\nimport sys\nfrom pathlib import Path\n\n# ---------------------------------------------------------------------------\n# Secret-sized alloca sizes (bytes) — common cryptographic key sizes\n# ---------------------------------------------------------------------------\n\nSECRET_ALLOCA_SIZES = {16, 24, 32, 48, 64, 96, 128}\n\n# Sensitive variable name pattern (matches LLVM SSA names)\nSENSITIVE_SSA_RE = re.compile(\n r\"(?i)%(\\w*(?:key|secret|password|token|nonce|seed|priv|master|credential)\\w*)\"\n)\n\n# ---------------------------------------------------------------------------\n# Finding counter\n# ---------------------------------------------------------------------------\n\n_finding_counter = [0]\n\n\ndef make_finding(\n category: str,\n severity: str,\n detail: str,\n file: str,\n line: int,\n symbol: str = \"\",\n confidence: str = \"likely\",\n) -> dict:\n _finding_counter[0] += 1\n fid = f\"F-RUST-IR-{_finding_counter[0]:04d}\"\n return {\n \"id\": fid,\n \"language\": \"rust\",\n \"category\": category,\n \"severity\": severity,\n \"confidence\": confidence,\n \"detail\": detail,\n \"symbol\": symbol,\n \"location\": {\"file\": file, \"line\": line},\n \"evidence\": [{\"source\": \"llvm_ir\", \"detail\": detail}],\n }\n\n\n# ---------------------------------------------------------------------------\n# IR helpers\n# ---------------------------------------------------------------------------\n\n\ndef count_volatile_stores(ir_text: str) -> int:\n return len(re.findall(r\"\\bstore volatile\\b\", ir_text))\n\n\ndef extract_volatile_stores_by_target(ir_text: str) -> dict[str, int]:\n \"\"\"\n Return volatile-store counts keyed by the destination symbol.\n Example matches:\n store volatile i8 0, ptr %key\n store volatile i32 0, i32* %buf\n \"\"\"\n stores: dict[str, int] = {}\n vol_re = re.compile(r\"\\bstore volatile\\b[^,]*,\\s*(?:ptr|i\\d+\\*)\\s+%([\\w\\.\\-]+)\")\n for m in vol_re.finditer(ir_text):\n name = m.group(1)\n stores[name] = stores.get(name, 0) + 1\n return stores\n\n\ndef extract_allocas(ir_text: str) -> dict[str, int]:\n \"\"\"\n Return {alloca_name: size_bytes} for fixed-size byte array allocas.\n Matches: %name = alloca [N x i8]\n \"\"\"\n alloca_re = re.compile(r\"%(\\w+)\\s*=\\s*alloca\\s+\\[(\\d+)\\s*x\\s*i8\\]\")\n allocas: dict[str, int] = {}\n for m in alloca_re.finditer(ir_text):\n allocas[m.group(1)] = int(m.group(2))\n return allocas\n\n\ndef extract_lifetime_ends(ir_text: str) -> set[str]:\n \"\"\"Return set of alloca names referenced in @llvm.lifetime.end calls.\"\"\"\n lifetime_re = re.compile(r\"call void @llvm\\.lifetime\\.end[^(]*\\([^,]+,\\s*(?:ptr|i8\\*)\\s+%(\\w+)\")\n return {m.group(1) for m in lifetime_re.finditer(ir_text)}\n\n\ndef extract_volatile_store_targets(ir_text: str) -> set[str]:\n \"\"\"Return set of symbols that receive volatile stores.\"\"\"\n return set(extract_volatile_stores_by_target(ir_text).keys())\n\n\ndef find_nonvolatile_memsets(ir_text: str) -> list[tuple[int, str]]:\n \"\"\"\n Return (lineno, line) for non-volatile @llvm.memset calls.\n Volatile variant is @llvm.memset.element.unordered.atomic or has i1 true volatile flag.\n \"\"\"\n results: list[tuple[int, str]] = []\n memset_re = re.compile(r\"call void @llvm\\.memset\\.\")\n volatile_flag_re = re.compile(r\"i1\\s+true\") # old-style volatile flag in args\n\n for lineno, line in enumerate(ir_text.splitlines(), start=1):\n if not memset_re.search(line):\n continue\n # Skip if it's the volatile atomic variant\n if \"unordered.atomic\" in line:\n continue\n # Skip if volatile flag (i1 true) is present in args\n if volatile_flag_re.search(line):\n continue\n results.append((lineno, line.strip()))\n return results\n\n\ndef find_secret_returns(ir_text: str) -> list[tuple[int, str]]:\n \"\"\"\n Detect returns of secret-named SSA values.\n Returns (lineno, symbol_without_percent).\n \"\"\"\n results: list[tuple[int, str]] = []\n ret_re = re.compile(\n r\"\\bret\\s+[^%]*%(\\w*(?:key|secret|password|token|nonce|seed|priv|master|credential)\\w*)\",\n re.IGNORECASE,\n )\n for lineno, line in enumerate(ir_text.splitlines(), start=1):\n m = ret_re.search(line)\n if m:\n results.append((lineno, m.group(1)))\n return results\n\n\ndef find_secret_aggregate_passes(ir_text: str) -> list[tuple[int, str]]:\n \"\"\"\n Detect call sites that appear to pass aggregate values containing secret-named\n symbols by value. This is heuristic and intentionally conservative.\n Returns (lineno, argument_snippet).\n \"\"\"\n results: list[tuple[int, str]] = []\n call_re = re.compile(r\"\\bcall\\s+\\S+\\s+@\\w+\\s*\\(([^)]*)\\)\")\n for lineno, line in enumerate(ir_text.splitlines(), start=1):\n m = call_re.search(line)\n if not m:\n continue\n args = m.group(1)\n if re.search(\n r\"%\\w*(?:key|secret|password|token|nonce|seed|priv|master|credential)\\w*\",\n args,\n re.IGNORECASE,\n ) and (\"{\" in args or \"byval\" in args):\n results.append((lineno, args[:120]))\n return results\n\n\ndef find_arg_load_calls(ir_text: str) -> list[tuple[int, str, str]]:\n \"\"\"\n Detect: %secret_val = load ... %secret_alloca followed by a call that uses %secret_val.\n Returns (lineno, varname, callee).\n \"\"\"\n results: list[tuple[int, str, str]] = []\n lines = ir_text.splitlines()\n\n load_re = re.compile(\n r\"(%\\w*(?:key|secret|password|token|nonce|seed)\\w*)\\s*=\\s*load\\b\", re.IGNORECASE\n )\n call_re = re.compile(r\"call\\s+\\S+\\s+(@\\w+)\\s*\\(([^)]*)\\)\")\n\n loaded_vars: dict[str, int] = {} # varname → lineno\n define_re = re.compile(r\"^define\\s\")\n\n for lineno, line in enumerate(lines, start=1):\n # Reset tracked loads at each LLVM IR function boundary to avoid\n # cross-function false positives (I17).\n if define_re.match(line):\n loaded_vars.clear()\n continue\n\n # Track loads of sensitive-named SSA values\n m = load_re.search(line)\n if m:\n loaded_vars[m.group(1)] = lineno\n continue\n\n # Check call sites\n mc = call_re.search(line)\n if not mc:\n continue\n callee = mc.group(1)\n if \"zeroize\" in callee.lower() or \"memset\" in callee.lower():\n continue\n args = mc.group(2)\n for varname, _load_lineno in loaded_vars.items():\n if varname in args:\n results.append((lineno, varname.lstrip(\"%\"), callee))\n\n return results\n\n\n# ---------------------------------------------------------------------------\n# Main analysis\n# ---------------------------------------------------------------------------\n\n\ndef analyze(level_to_ir: dict[str, tuple[str, str]]) -> list[dict]:\n \"\"\"Analyze LLVM IR files for zeroization issues.\n\n Precondition: ``level_to_ir`` must contain at least ``\"O0\"`` and ``\"O2\"``\n keys — if either is absent the function returns an empty list with no\n diagnostic. The CLI always satisfies this; library callers must ensure it.\n \"\"\"\n findings: list[dict] = []\n if \"O0\" not in level_to_ir or \"O2\" not in level_to_ir:\n return findings\n\n o0_file, o0_text = level_to_ir[\"O0\"]\n o2_file, o2_text = level_to_ir[\"O2\"]\n\n # --- 1. Global volatile store count drop O0 → O2 ---\n o0_vol_count = count_volatile_stores(o0_text)\n o2_vol_count = count_volatile_stores(o2_text)\n\n if o0_vol_count > o2_vol_count:\n diff = o0_vol_count - o2_vol_count\n # line=0 is used for file-level findings that cannot be attributed to a\n # single source line (I18). Downstream consumers should treat line 0\n # as \"file-level / unknown line\".\n findings.append(\n make_finding(\n \"OPTIMIZED_AWAY_ZEROIZE\",\n \"high\",\n f\"Volatile store count dropped from {o0_vol_count} (O0) to {o2_vol_count} (O2) \"\n f\"— {diff} volatile wipe(s) eliminated by dead-store elimination\",\n o2_file,\n 0,\n )\n )\n\n # --- 1b. Per-target volatile store drop O0 -> O2 (hard evidence by symbol) ---\n o0_vol_by_target = extract_volatile_stores_by_target(o0_text)\n o2_vol_by_target = extract_volatile_stores_by_target(o2_text)\n for target, o0_count in sorted(o0_vol_by_target.items()):\n o2_count = o2_vol_by_target.get(target, 0)\n if o0_count > o2_count:\n findings.append(\n make_finding(\n \"OPTIMIZED_AWAY_ZEROIZE\",\n \"high\",\n f\"Volatile stores to %{target} dropped from {o0_count} (O0) to {o2_count} (O2) \"\n f\"— symbol-specific wipe elimination detected\",\n o2_file,\n 0,\n symbol=target,\n )\n )\n\n # --- 2. Non-volatile llvm.memset calls in O2 IR ---\n for lineno, line_text in find_nonvolatile_memsets(o2_text):\n findings.append(\n make_finding(\n \"OPTIMIZED_AWAY_ZEROIZE\",\n \"high\",\n f\"Non-volatile @llvm.memset in O2 IR — DSE-eligible, may be removed at higher \"\n f\"optimization. Use zeroize crate or volatile memset. IR: {line_text[:80]}\",\n o2_file,\n lineno,\n )\n )\n\n # --- 3. alloca with lifetime.end but no volatile store (STACK_RETENTION) ---\n o2_allocas = extract_allocas(o2_text)\n o2_lifetime_ends = extract_lifetime_ends(o2_text)\n o2_vol_targets = extract_volatile_store_targets(o2_text)\n\n for alloca_name, size in o2_allocas.items():\n if size not in SECRET_ALLOCA_SIZES:\n continue\n if alloca_name not in o2_lifetime_ends:\n continue\n if alloca_name in o2_vol_targets:\n continue\n findings.append(\n make_finding(\n \"STACK_RETENTION\",\n \"high\",\n f\"alloca [{size} x i8] %{alloca_name} has @llvm.lifetime.end but no \"\n \"volatile store — stack bytes not wiped before slot is freed\",\n o2_file,\n 0,\n symbol=alloca_name,\n )\n )\n\n # --- 4. SROA/mem2reg: secret alloca present at O0 but absent at O2 ---\n o0_allocas = extract_allocas(o0_text)\n\n o0_vol_targets = extract_volatile_store_targets(o0_text)\n for alloca_name, size in o0_allocas.items():\n if size not in SECRET_ALLOCA_SIZES:\n continue\n if alloca_name in o2_allocas:\n continue\n # Hard evidence gate: only emit when O0 showed a wipe target on this alloca.\n if alloca_name not in o0_vol_targets:\n continue\n findings.append(\n make_finding(\n \"OPTIMIZED_AWAY_ZEROIZE\",\n \"high\",\n f\"alloca [{size} x i8] %{alloca_name} present at O0 but absent at O2 — \"\n \"SROA/mem2reg promoted it to registers; any volatile stores targeting this \"\n \"alloca are now unreachable\",\n o2_file,\n 0,\n symbol=alloca_name,\n )\n )\n\n # --- 5. Secret value in argument registers at call site (REGISTER_SPILL) ---\n for lineno, varname, callee in find_arg_load_calls(o2_text):\n findings.append(\n make_finding(\n \"REGISTER_SPILL\",\n \"medium\",\n f\"Secret-named SSA value '%{varname}' loaded and passed directly to \"\n f\"'{callee}' — value in argument register may not be cleared after call\",\n o2_file,\n lineno,\n symbol=varname,\n )\n )\n\n # --- 6. Secret return values can persist in return registers ---\n for lineno, varname in find_secret_returns(o2_text):\n findings.append(\n make_finding(\n \"REGISTER_SPILL\",\n \"medium\",\n f\"Secret-named SSA value '%{varname}' is returned directly — \"\n \"value may persist in return registers after function exit\",\n o2_file,\n lineno,\n symbol=varname,\n )\n )\n\n # --- 7. Aggregate/by-value secret argument passing ---\n for lineno, snippet in find_secret_aggregate_passes(o2_text):\n findings.append(\n make_finding(\n \"SECRET_COPY\",\n \"medium\",\n \"Potential by-value aggregate call argument contains secret-named data; \"\n f\"copy may escape zeroization tracking. Args: {snippet}\",\n o2_file,\n lineno,\n )\n )\n\n # Collect targets already reported in section 1b (O0→O2 per-symbol comparison)\n # so that the multi-level section below does not re-emit the same target.\n reported_by_1b: set[str] = {\n target\n for target, o0_count in o0_vol_by_target.items()\n if o0_count > o2_vol_by_target.get(target, 0)\n }\n\n # --- 8. Optional multi-level comparison (O0->O1->O2, O2->O3) ---\n # Skip the (O0, O2) adjacent pair when O1 is absent — that comparison is already\n # done by sections 1 and 1b above, and re-emitting it here causes duplicate findings.\n level_order = [\"O0\", \"O1\", \"O2\", \"O3\"]\n present = [lvl for lvl in level_order if lvl in level_to_ir]\n for idx in range(len(present) - 1):\n from_level = present[idx]\n to_level = present[idx + 1]\n # O0→O2 without an intermediate O1 is already covered by sections 1/1b.\n if from_level == \"O0\" and to_level == \"O2\":\n continue\n _, from_ir = level_to_ir[from_level]\n to_file, to_ir = level_to_ir[to_level]\n from_targets = extract_volatile_stores_by_target(from_ir)\n to_targets = extract_volatile_stores_by_target(to_ir)\n for target, from_count in sorted(from_targets.items()):\n # Skip targets already covered by section 1b to avoid cascading duplicates.\n if target in reported_by_1b:\n continue\n to_count = to_targets.get(target, 0)\n if from_count > to_count:\n findings.append(\n make_finding(\n \"OPTIMIZED_AWAY_ZEROIZE\",\n \"high\",\n f\"Volatile stores to %{target} dropped from {from_count} ({from_level}) \"\n f\"to {to_count} ({to_level})\",\n to_file,\n 0,\n symbol=target,\n )\n )\n\n return findings\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\n\ndef main() -> int:\n parser = argparse.ArgumentParser(\n description=\"LLVM IR O0 vs O2 comparison for Rust dead-store-elimination findings\"\n )\n parser.add_argument(\"--o0\", required=True, help=\"Path to O0 .ll file\")\n parser.add_argument(\"--o2\", required=True, help=\"Path to O2 .ll file\")\n parser.add_argument(\"--o1\", required=False, help=\"Path to O1 .ll file (optional)\")\n parser.add_argument(\"--o3\", required=False, help=\"Path to O3 .ll file (optional)\")\n parser.add_argument(\"--out\", required=True, help=\"Output findings JSON path\")\n args = parser.parse_args()\n\n level_paths: dict[str, Path] = {\n \"O0\": Path(args.o0),\n \"O2\": Path(args.o2),\n }\n if args.o1:\n level_paths[\"O1\"] = Path(args.o1)\n if args.o3:\n level_paths[\"O3\"] = Path(args.o3)\n\n for p in level_paths.values():\n if not p.exists():\n print(f\"check_llvm_patterns.py: IR file not found: {p}\", file=sys.stderr)\n return 1\n\n level_to_ir: dict[str, tuple[str, str]] = {}\n try:\n for level, path in level_paths.items():\n level_to_ir[level] = (str(path), path.read_text(encoding=\"utf-8\", errors=\"replace\"))\n except OSError as e:\n print(f\"check_llvm_patterns.py: failed to read IR: {e}\", file=sys.stderr)\n return 1\n\n findings = analyze(level_to_ir)\n\n out_path = Path(args.out)\n out_path.parent.mkdir(parents=True, exist_ok=True)\n out_path.write_text(json.dumps(findings, indent=2), encoding=\"utf-8\")\n\n print(f\"check_llvm_patterns.py: {len(findings)} finding(s) written to {out_path}\")\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":17576,"content_sha256":"0a17e661233fa48feab2b84fa08b685632bef51c8a479c0504e2de6369e73b5c"},{"filename":"tools/scripts/check_mir_patterns.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\ncheck_mir_patterns.py — MIR text pattern analysis for Rust zeroization issues.\n\nReads a Rust MIR file (emitted by emit_rust_mir.sh) and a sensitive-objects JSON\nfile, then detects patterns indicative of missing or incorrect zeroization.\n\nAll analysis is text/regex based — no MIR parser required.\n\nUsage:\n uv run check_mir_patterns.py \\\n --mir \u003cpath.mir> --secrets \u003csensitive-objects.json> --out \u003cfindings.json>\n\nExit codes:\n 0 — ran successfully (findings may be empty)\n 1 — input file not found\n 2 — argument error\n\"\"\"\n\nimport argparse\nimport json\nimport re\nimport sys\nfrom pathlib import Path\n\n# ---------------------------------------------------------------------------\n# Sensitive name patterns (applied to local variable names in MIR)\n# ---------------------------------------------------------------------------\n\nSENSITIVE_LOCAL_RE = re.compile(\n # Match keyword not preceded/followed by a letter so that compound names\n # like 'secret_key', 'private_key', and 'auth_token' are correctly matched\n # while avoiding spurious hits on words like 'monkey' or 'tokenize'.\n r\"(?i)(?\u003c![a-zA-Z])(key|secret|password|token|nonce|seed|priv|master|credential)(?![a-zA-Z])\"\n)\n\n# ---------------------------------------------------------------------------\n# Finding counter\n# ---------------------------------------------------------------------------\n\n_finding_counter = [0]\n\n\ndef make_finding(\n category: str,\n severity: str,\n detail: str,\n file: str,\n line: int,\n symbol: str = \"\",\n confidence: str = \"likely\",\n) -> dict:\n _finding_counter[0] += 1\n fid = f\"F-RUST-MIR-{_finding_counter[0]:04d}\"\n return {\n \"id\": fid,\n \"language\": \"rust\",\n \"category\": category,\n \"severity\": severity,\n \"confidence\": confidence,\n \"detail\": detail,\n \"symbol\": symbol,\n \"location\": {\"file\": file, \"line\": line},\n \"evidence\": [{\"source\": \"mir_text\", \"detail\": detail}],\n }\n\n\n# ---------------------------------------------------------------------------\n# MIR parsing helpers\n# ---------------------------------------------------------------------------\n\n\ndef split_into_functions(mir_text: str) -> list[tuple[str, list[str], int]]:\n \"\"\"\n Split MIR text into (fn_name, body_lines, start_lineno) tuples.\n MIR functions start with 'fn \u003cname>' or 'mir_body' headers.\n \"\"\"\n functions: list[tuple[str, list[str], int]] = []\n lines = mir_text.splitlines()\n fn_re = re.compile(r\"^fn\\s+(\\S+)\\s*\\(\")\n current_name = \"\u003ctop>\"\n current_lines: list[str] = []\n current_start = 0\n depth = 0\n\n for lineno, line in enumerate(lines, start=1):\n m = fn_re.match(line.strip())\n if m and depth == 0:\n if current_lines:\n functions.append((current_name, current_lines, current_start))\n current_name = m.group(1)\n current_lines = [line]\n current_start = lineno\n depth = line.count(\"{\") - line.count(\"}\")\n else:\n current_lines.append(line)\n depth += line.count(\"{\") - line.count(\"}\")\n if depth \u003c 0:\n print(\n f\"check_mir_patterns.py: warning: negative brace depth at line {lineno} \"\n f\"in {current_name!r} — MIR may be malformed\",\n file=sys.stderr,\n )\n depth = 0\n\n if current_lines:\n functions.append((current_name, current_lines, current_start))\n\n return functions\n\n\ndef local_names_from_debug_info(fn_lines: list[str]) -> dict[str, str]:\n \"\"\"\n Extract MIR debug variable map: local slot → variable name.\n MIR debug lines look like: debug varname => _5;\n \"\"\"\n mapping: dict[str, str] = {}\n debug_re = re.compile(r\"debug\\s+(\\w+)\\s*=>\\s*(_\\d+)\")\n for line in fn_lines:\n m = debug_re.search(line)\n if m:\n varname, slot = m.group(1), m.group(2)\n mapping[slot] = varname\n return mapping\n\n\ndef is_sensitive_local(\n slot: str, debug_map: dict[str, str], sensitive_re: re.Pattern[str] = SENSITIVE_LOCAL_RE\n) -> bool:\n varname = debug_map.get(slot, \"\")\n return bool(sensitive_re.search(varname))\n\n\ndef is_zeroizing_type(type_name: str) -> bool:\n return bool(re.search(r\"(?i)(Zeroiz|ZeroizeOnDrop|SecretBox|Zeroizing)\", type_name))\n\n\n# ---------------------------------------------------------------------------\n# Pattern detectors\n# ---------------------------------------------------------------------------\n\n\ndef detect_drop_before_storagedead(\n fn_name: str,\n fn_lines: list[str],\n fn_start: int,\n debug_map: dict[str, str],\n mir_file: str,\n sensitive_re: re.Pattern[str] = SENSITIVE_LOCAL_RE,\n) -> list[dict]:\n \"\"\"\n Pattern: drop(_X) present but StorageDead(_X) absent for any sensitive local.\n Indicates the local may persist on stack after the drop.\n \"\"\"\n findings: list[dict] = []\n drop_re = re.compile(r\"\\bdrop\\(_(\\d+)\\)\")\n storagedead_re = re.compile(r\"StorageDead\\(_(\\d+)\\)\")\n\n dropped: set[str] = set()\n storage_dead: set[str] = set()\n\n for line in fn_lines:\n for m in drop_re.finditer(line):\n dropped.add(f\"_{m.group(1)}\")\n for m in storagedead_re.finditer(line):\n storage_dead.add(f\"_{m.group(1)}\")\n\n has_return = any(re.search(r\"\\breturn\\b\", line) for line in fn_lines)\n\n for slot in dropped - storage_dead:\n if not is_sensitive_local(slot, debug_map, sensitive_re):\n continue\n if has_return:\n # Prefer the path-sensitive NOT_ON_ALL_PATHS finding over the generic\n # MISSING_SOURCE_ZEROIZE to avoid emitting duplicate findings for the\n # same slot (C7: both fired before for slots with explicit return paths).\n findings.append(\n make_finding(\n \"NOT_ON_ALL_PATHS\",\n \"high\",\n f\"Secret local {slot} ({debug_map.get(slot, '?')!r}) is dropped but not \"\n f\"StorageDead on explicit return path(s) in '{fn_name}'\",\n mir_file,\n fn_start,\n symbol=debug_map.get(slot, slot),\n )\n )\n else:\n findings.append(\n make_finding(\n \"MISSING_SOURCE_ZEROIZE\",\n \"medium\",\n f\"Secret local {slot} ({debug_map.get(slot, '?')!r}) is dropped without \"\n f\"StorageDead in '{fn_name}' — verify zeroize call in drop glue\",\n mir_file,\n fn_start,\n symbol=debug_map.get(slot, slot),\n )\n )\n\n return findings\n\n\ndef detect_resume_with_live_secrets(\n fn_name: str,\n fn_lines: list[str],\n fn_start: int,\n debug_map: dict[str, str],\n mir_file: str,\n sensitive_re: re.Pattern[str] = SENSITIVE_LOCAL_RE,\n) -> list[dict]:\n \"\"\"\n Pattern: 'resume' terminator (unwind/panic path) with sensitive locals in scope.\n \"\"\"\n findings: list[dict] = []\n resume_re = re.compile(r\"\\bresume\\b\")\n has_resume = any(resume_re.search(line) for line in fn_lines)\n if not has_resume:\n return findings\n\n sensitive_locals = [\n slot for slot in debug_map if is_sensitive_local(slot, debug_map, sensitive_re)\n ]\n if sensitive_locals:\n names = [debug_map[s] for s in sensitive_locals[:3]]\n findings.append(\n make_finding(\n \"MISSING_SOURCE_ZEROIZE\",\n \"medium\",\n f\"Panic/unwind path (resume) in '{fn_name}' with sensitive \"\n f\"locals {names} in scope — verify these locals are dropped \"\n \"(and zeroed) on the unwind path\",\n mir_file,\n fn_start,\n symbol=names[0] if names else \"\",\n )\n )\n return findings\n\n\ndef detect_aggregate_move_non_zeroizing(\n fn_name: str,\n fn_lines: list[str],\n fn_start: int,\n debug_map: dict[str, str],\n mir_file: str,\n sensitive_re: re.Pattern[str] = SENSITIVE_LOCAL_RE,\n) -> list[dict]:\n \"\"\"\n Pattern: _Y = TypeName { field: move _X } where _X is a sensitive local\n and TypeName does not appear to be a Zeroizing wrapper.\n \"\"\"\n findings: list[dict] = []\n agg_re = re.compile(r\"(_\\d+)\\s*=\\s*(\\w[\\w:]*)\\s*\\{[^}]*move\\s+(_\\d+)\")\n\n for lineno, line in enumerate(fn_lines, start=fn_start):\n m = agg_re.search(line)\n if not m:\n continue\n _dest, type_name, _src = m.group(1), m.group(2), m.group(3)\n if is_sensitive_local(_src, debug_map, sensitive_re) and not is_zeroizing_type(type_name):\n src_name = debug_map.get(_src, _src)\n findings.append(\n make_finding(\n \"SECRET_COPY\",\n \"medium\",\n f\"Secret local '{src_name}' moved into non-Zeroizing aggregate '{type_name}' \"\n f\"in '{fn_name}' — copy now untracked\",\n mir_file,\n lineno,\n symbol=src_name,\n )\n )\n return findings\n\n\ndef detect_closure_capture_secret(\n fn_name: str,\n fn_lines: list[str],\n fn_start: int,\n debug_map: dict[str, str],\n mir_file: str,\n sensitive_re: re.Pattern[str] = SENSITIVE_LOCAL_RE,\n) -> list[dict]:\n \"\"\"\n Pattern: closure/async state captures a sensitive local by move.\n \"\"\"\n findings: list[dict] = []\n closure_re = re.compile(\n r\"(_\\d+)\\s*=\\s*.*(?:closure|async|generator|Coroutine).*move\\s+(_\\d+)\",\n re.IGNORECASE,\n )\n for lineno, line in enumerate(fn_lines, start=fn_start):\n m = closure_re.search(line)\n if not m:\n continue\n captured_slot = m.group(2)\n if is_sensitive_local(captured_slot, debug_map, sensitive_re):\n name = debug_map.get(captured_slot, captured_slot)\n findings.append(\n make_finding(\n \"SECRET_COPY\",\n \"high\",\n f\"Sensitive local '{name}' is captured by move into a closure/async state \"\n f\"in '{fn_name}' — copy may outlive intended wipe scope\",\n mir_file,\n lineno,\n symbol=name,\n )\n )\n return findings\n\n\ndef detect_drop_glue_without_zeroize(\n fn_name: str, fn_lines: list[str], fn_start: int, mir_file: str\n) -> list[dict]:\n \"\"\"\n Pattern: function is a drop glue (drop_in_place / _drop_impl) and contains\n drop(_X) but no call to zeroize::.\n \"\"\"\n if not re.search(r\"(drop_in_place|_drop_impl)\", fn_name):\n return []\n\n findings: list[dict] = []\n has_drop_call = any(re.search(r\"\\bdrop\\(_\\d+\\)\", line) for line in fn_lines)\n has_zeroize_call = any(re.search(r\"\\bzeroize::\", line) for line in fn_lines)\n\n if has_drop_call and not has_zeroize_call:\n findings.append(\n make_finding(\n \"MISSING_SOURCE_ZEROIZE\",\n \"high\",\n f\"Drop glue '{fn_name}' calls drop() but no call to zeroize:: found — \"\n \"secret not wiped on drop\",\n mir_file,\n fn_start,\n symbol=fn_name,\n )\n )\n return findings\n\n\ndef detect_ffi_call_with_secret(\n fn_name: str,\n fn_lines: list[str],\n fn_start: int,\n debug_map: dict[str, str],\n mir_file: str,\n sensitive_re: re.Pattern[str] = SENSITIVE_LOCAL_RE,\n) -> list[dict]:\n \"\"\"\n Pattern: extern \"C\" call with a sensitive local as an argument.\n In MIR: extern fns are called with ABI specifier; we look for\n 'extern \"C\"' in fn declaration context and call sites with sensitive locals.\n \"\"\"\n findings: list[dict] = []\n call_re = re.compile(r\"\\bcall\\s+(\\S+)\\s*\\(([^)]*)\\)\")\n\n # In MIR, extern fn calls appear as calls to paths containing \"extern_C\" or similar.\n # Heuristic: look for call sites that pass a sensitive local as an argument.\n for lineno, line in enumerate(fn_lines, start=fn_start):\n m = call_re.search(line)\n if not m:\n continue\n callee = m.group(1)\n args_text = m.group(2)\n # Check if any argument is a sensitive local\n arg_slots = re.findall(r\"_(\\d+)\", args_text)\n for slot_num in arg_slots:\n slot = f\"_{slot_num}\"\n if is_sensitive_local(slot, debug_map, sensitive_re):\n # Check if the callee looks like an FFI function (not zeroize::)\n if \"zeroize\" in callee.lower():\n continue\n # Look for extern \"C\" indication — either in callee name or nearby\n if re.search(r\"(::c_|_ffi_|_sys_|extern)\", callee, re.IGNORECASE):\n src_name = debug_map.get(slot, slot)\n findings.append(\n make_finding(\n \"SECRET_COPY\",\n \"high\",\n f\"Secret local '{src_name}' passed to potential FFI call '{callee}' \"\n f\"in '{fn_name}' — zeroization guarantees lost in callee\",\n mir_file,\n lineno,\n symbol=src_name,\n )\n )\n return findings\n\n\ndef detect_yield_with_live_secret(\n fn_name: str,\n fn_lines: list[str],\n fn_start: int,\n debug_map: dict[str, str],\n mir_file: str,\n sensitive_re: re.Pattern[str] = SENSITIVE_LOCAL_RE,\n) -> list[dict]:\n \"\"\"\n Pattern: Yield terminator (async/coroutine state machine) with sensitive-named\n locals that could be live at the yield point.\n \"\"\"\n findings: list[dict] = []\n yield_re = re.compile(r\"\\byield\\b\")\n has_yield = any(yield_re.search(line) for line in fn_lines)\n if not has_yield:\n return findings\n\n sensitive_locals = [\n slot for slot in debug_map if is_sensitive_local(slot, debug_map, sensitive_re)\n ]\n if sensitive_locals:\n names = [debug_map[s] for s in sensitive_locals[:3]]\n findings.append(\n make_finding(\n \"NOT_ON_ALL_PATHS\",\n \"high\",\n f\"Coroutine/async fn '{fn_name}' has Yield terminator with sensitive locals \"\n f\"{names} potentially live at suspension point — secrets stored in heap-allocated \"\n \"Future state machine; ZeroizeOnDrop covers stack variables only\",\n mir_file,\n fn_start,\n symbol=names[0] if names else \"\",\n )\n )\n return findings\n\n\ndef detect_result_err_path_with_secret(\n fn_name: str,\n fn_lines: list[str],\n fn_start: int,\n debug_map: dict[str, str],\n mir_file: str,\n sensitive_re: re.Pattern[str] = SENSITIVE_LOCAL_RE,\n) -> list[dict]:\n \"\"\"\n Pattern: explicit error-path style return (`Err(...)`) while sensitive locals\n are still in scope.\n \"\"\"\n findings: list[dict] = []\n err_re = re.compile(r\"\\bErr\\s*\\(\")\n if not any(err_re.search(line) for line in fn_lines):\n return findings\n sensitive_locals = [\n slot for slot in debug_map if is_sensitive_local(slot, debug_map, sensitive_re)\n ]\n if not sensitive_locals:\n return findings\n names = [debug_map[s] for s in sensitive_locals[:3]]\n findings.append(\n make_finding(\n \"NOT_ON_ALL_PATHS\",\n \"high\",\n f\"Potential Result::Err early-return path in '{fn_name}' with sensitive locals {names} \"\n \"still in scope — verify cleanup on all error exits\",\n mir_file,\n fn_start,\n symbol=names[0] if names else \"\",\n )\n )\n return findings\n\n\n# ---------------------------------------------------------------------------\n# Main analysis\n# ---------------------------------------------------------------------------\n\n\ndef analyze(mir_text: str, sensitive_objects: list[dict], mir_file: str) -> list[dict]:\n findings: list[dict] = []\n functions = split_into_functions(mir_text)\n\n extra_names = [obj.get(\"name\", \"\") for obj in sensitive_objects if obj.get(\"name\")]\n sensitive_re = SENSITIVE_LOCAL_RE\n if extra_names:\n augmented = (\n SENSITIVE_LOCAL_RE.pattern\n + \"|\"\n + \"|\".join(r\"\\b\" + re.escape(n) + r\"\\b\" for n in extra_names)\n )\n sensitive_re = re.compile(augmented, re.IGNORECASE)\n\n for fn_name, fn_lines, fn_start in functions:\n debug_map = local_names_from_debug_info(fn_lines)\n\n ctx = (fn_name, fn_lines, fn_start)\n findings.extend(detect_drop_before_storagedead(*ctx, debug_map, mir_file, sensitive_re))\n findings.extend(detect_resume_with_live_secrets(*ctx, debug_map, mir_file, sensitive_re))\n findings.extend(\n detect_aggregate_move_non_zeroizing(*ctx, debug_map, mir_file, sensitive_re)\n )\n findings.extend(detect_closure_capture_secret(*ctx, debug_map, mir_file, sensitive_re))\n findings.extend(detect_drop_glue_without_zeroize(*ctx, mir_file))\n findings.extend(detect_ffi_call_with_secret(*ctx, debug_map, mir_file, sensitive_re))\n findings.extend(detect_yield_with_live_secret(*ctx, debug_map, mir_file, sensitive_re))\n findings.extend(detect_result_err_path_with_secret(*ctx, debug_map, mir_file, sensitive_re))\n\n return findings\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\n\ndef main() -> int:\n parser = argparse.ArgumentParser(\n description=\"MIR text pattern analysis for Rust zeroization issues\"\n )\n parser.add_argument(\"--mir\", required=True, help=\"Path to .mir file\")\n parser.add_argument(\"--secrets\", required=True, help=\"Path to sensitive-objects.json\")\n parser.add_argument(\"--out\", required=True, help=\"Output findings JSON path\")\n args = parser.parse_args()\n\n mir_path = Path(args.mir)\n if not mir_path.exists():\n print(f\"check_mir_patterns.py: MIR file not found: {mir_path}\", file=sys.stderr)\n return 1\n\n secrets_path = Path(args.secrets)\n if not secrets_path.exists():\n print(f\"check_mir_patterns.py: secrets file not found: {secrets_path}\", file=sys.stderr)\n return 1\n\n try:\n mir_text = mir_path.read_text(encoding=\"utf-8\", errors=\"replace\")\n except OSError as e:\n print(f\"check_mir_patterns.py: failed to read MIR: {e}\", file=sys.stderr)\n return 1\n\n try:\n sensitive_objects = json.loads(secrets_path.read_text(encoding=\"utf-8\", errors=\"replace\"))\n except (json.JSONDecodeError, OSError) as e:\n print(f\"check_mir_patterns.py: failed to parse secrets JSON: {e}\", file=sys.stderr)\n return 1\n\n findings = analyze(mir_text, sensitive_objects, str(mir_path))\n\n out_path = Path(args.out)\n out_path.parent.mkdir(parents=True, exist_ok=True)\n out_path.write_text(json.dumps(findings, indent=2), encoding=\"utf-8\")\n\n print(f\"check_mir_patterns.py: {len(findings)} finding(s) written to {out_path}\")\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":19399,"content_sha256":"6591eac1ea39b5e2e4cab93fcc01a3f9db6ea421676583bb31e6677140ff6cef"},{"filename":"tools/scripts/check_rust_asm_aarch64.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\ncheck_rust_asm_aarch64.py — AArch64 Rust assembly analysis backend.\n\n⚠ EXPERIMENTAL — AArch64 support is incomplete. Findings should be treated as\n indicative only and require manual verification before inclusion in a report.\n\n Known limitations:\n - x29 (frame pointer) and x30 (link register) are always saved in the prologue\n via `stp x29, x30, [sp, #-N]!`. These appear as REGISTER_SPILL findings\n because both are in AARCH64_CALLEE_SAVED. They are almost never carrying\n secret values — reviewers should verify in context.\n - `dc zva` (Data Cache Zero by Virtual Address) is not detected as a zero-store.\n This instruction is rare in Rust-generated code but may be used in\n highly-optimised zeroize implementations.\n - AArch64 has no red zone (neither Linux nor macOS AAPCS64 define one). Leaf\n functions must allocate stack space explicitly; no red-zone analysis needed.\n - Apple AArch64 (M1/M2) and Linux AArch64 both use AAPCS64 with no red zone;\n the analysis is platform-agnostic.\n\nCalled by check_rust_asm.py. Not intended for direct invocation.\n\"\"\"\n\nimport re\n\n# ---------------------------------------------------------------------------\n# AArch64 register sets (AAPCS64)\n# ---------------------------------------------------------------------------\n\nAARCH64_CALLER_SAVED = {\n # Integer/pointer: argument registers and temporaries\n \"x0\",\n \"x1\",\n \"x2\",\n \"x3\",\n \"x4\",\n \"x5\",\n \"x6\",\n \"x7\",\n \"x8\",\n \"x9\",\n \"x10\",\n \"x11\",\n \"x12\",\n \"x13\",\n \"x14\",\n \"x15\",\n \"x16\",\n \"x17\",\n # SIMD/FP: v0–v7 and v16–v31 are caller-saved (argument/scratch)\n \"v0\",\n \"v1\",\n \"v2\",\n \"v3\",\n \"v4\",\n \"v5\",\n \"v6\",\n \"v7\",\n \"v16\",\n \"v17\",\n \"v18\",\n \"v19\",\n \"v20\",\n \"v21\",\n \"v22\",\n \"v23\",\n \"v24\",\n \"v25\",\n \"v26\",\n \"v27\",\n \"v28\",\n \"v29\",\n \"v30\",\n \"v31\",\n}\n\nAARCH64_CALLEE_SAVED = {\n # Integer: x19–x28 must be preserved if used\n \"x19\",\n \"x20\",\n \"x21\",\n \"x22\",\n \"x23\",\n \"x24\",\n \"x25\",\n \"x26\",\n \"x27\",\n \"x28\",\n # x29 = frame pointer (fp), x30 = link register (lr)\n # NOTE: x29 and x30 are always saved in prologues; see limitations above.\n \"x29\",\n \"x30\",\n # SIMD/FP: lower 64 bits of v8–v15 must be preserved\n \"v8\",\n \"v9\",\n \"v10\",\n \"v11\",\n \"v12\",\n \"v13\",\n \"v14\",\n \"v15\",\n}\n\n# ---------------------------------------------------------------------------\n# Patterns (ARM GNU syntax, as emitted by LLVM for AArch64)\n# ---------------------------------------------------------------------------\n\n# Frame allocation\n# Most common: pre-index pair store that saves fp/lr and decrements sp\nRE_A64_FRAME_STP = re.compile(r\"stp\\s+x29,\\s+x30,\\s+\\[sp,\\s+#-(\\d+)\\]!\")\n# Alternative: explicit sub\nRE_A64_FRAME_SUB = re.compile(r\"sub\\s+sp,\\s+sp,\\s+#(\\d+)\")\n\n# Zero-store patterns\n# str xzr/wzr, [sp, #N] — single 64-bit (xzr) or 32-bit (wzr) zero store to stack\nRE_A64_STR_XZR = re.compile(r\"\\bstr\\s+[xw]zr,\\s+\\[sp(?:,\\s*#-?\\d+)?\\]\")\n# stp xzr, xzr / wzr, wzr, [sp, #N] — paired zero store (most efficient)\nRE_A64_STP_XZR = re.compile(r\"\\bstp\\s+[xw]zr,\\s+[xw]zr,\\s+\\[sp(?:,\\s*#-?\\d+)?\\]\")\n# movi vN.*, #0 — SIMD register zeroing (precedes stp qN)\nRE_A64_MOVI_ZERO = re.compile(r\"\\bmovi\\s+v\\d+\\.\\w+,\\s+#0\\b\")\n# bl ...(memset|zeroize) — call to zeroize/memset routine\nRE_A64_MEMSET = re.compile(r\"\\bbl\\s+.*(?:memset|volatile_set_memory|zeroize)\")\n\n# Register spill patterns\n# str xN/vN/qN, [sp, #offset] — single store to stack\nRE_A64_STR_SPILL = re.compile(r\"\\bstr\\s+(x\\d+|v\\d+|q\\d+),\\s+\\[sp(?:,\\s*#-?\\d+)?\\]\")\n# stp xN, xM / qN, qM, [sp, #offset] — pair store to stack (I31: also covers SIMD q pairs)\nRE_A64_STP_SPILL = re.compile(r\"\\bstp\\s+((?:x|q)\\d+),\\s+((?:x|q)\\d+),\\s+\\[sp(?:,\\s*#-?\\d+)?\\]\")\n\n# Return instruction (no suffix on AArch64, unlike x86-64's retq)\nRE_A64_RET = re.compile(r\"\\bret\\b\")\n\n\n# ---------------------------------------------------------------------------\n# STACK_RETENTION (AArch64)\n# ---------------------------------------------------------------------------\n\n\ndef check_stack_retention(\n func_name: str,\n func_lines: list[tuple[int, str]],\n) -> dict | None:\n \"\"\"\n Detect AArch64 stack frame allocated but not zeroed before return.\n\n [EXPERIMENTAL] Findings require manual verification.\n \"\"\"\n frame_alloc_line: tuple[int, str] | None = None\n frame_size = 0\n has_zero_store = False\n ret_line: tuple[int, str] | None = None\n\n for lineno, line in func_lines:\n # stp x29, x30, [sp, #-N]! — most common AArch64 prologue (pre-index)\n m = RE_A64_FRAME_STP.search(line)\n if m:\n if frame_alloc_line is None:\n frame_alloc_line = (lineno, line.strip())\n frame_size += int(m.group(1))\n\n # sub sp, sp, #N — additional explicit allocation (common with stp prologue)\n # Accumulate rather than taking only the first allocation so that prologues\n # using both stp+sub report the correct total frame size (I28).\n m2 = RE_A64_FRAME_SUB.search(line)\n if m2:\n if frame_alloc_line is None:\n frame_alloc_line = (lineno, line.strip())\n frame_size += int(m2.group(1))\n\n # Zero-store detection\n if RE_A64_STR_XZR.search(line) or RE_A64_STP_XZR.search(line):\n has_zero_store = True\n if RE_A64_MOVI_ZERO.search(line) or RE_A64_MEMSET.search(line):\n has_zero_store = True\n\n if RE_A64_RET.search(line):\n ret_line = (lineno, line.strip())\n\n if frame_alloc_line and ret_line and not has_zero_store and frame_size > 0:\n alloc_lineno, alloc_text = frame_alloc_line\n ret_lineno, _ = ret_line\n return {\n \"category\": \"STACK_RETENTION\",\n \"severity\": \"high\",\n \"symbol\": func_name,\n \"detail\": (\n f\"[EXPERIMENTAL] AArch64 stack frame of {frame_size} bytes allocated \"\n f\"at line {alloc_lineno} ({alloc_text!r}) but no zero-store \"\n f\"(str xzr / stp xzr,xzr / movi+stp / zeroize call) found \"\n f\"before return at line {ret_lineno}\"\n ),\n \"evidence_detail\": (\n f\"{alloc_text} at line {alloc_lineno}; \"\n f\"no str/stp xzr or zeroize call before ret at line {ret_lineno}\"\n ),\n }\n return None\n\n\n# ---------------------------------------------------------------------------\n# REGISTER_SPILL (AArch64)\n# ---------------------------------------------------------------------------\n\n\ndef check_register_spill(\n func_name: str,\n func_lines: list[tuple[int, str]],\n) -> list[dict]:\n \"\"\"\n Detect AArch64 registers spilled to the stack.\n\n [EXPERIMENTAL] x29/x30 prologue saves will always appear here because both\n are in AARCH64_CALLEE_SAVED. Reviewers should check whether those registers\n actually hold sensitive values in the function under analysis.\n \"\"\"\n spills: list[tuple[int, str, str]] = [] # (lineno, reg, line)\n\n for lineno, line in func_lines:\n # Single store: str xN/vN/qN, [sp, ...]\n m = RE_A64_STR_SPILL.search(line)\n if m:\n reg = m.group(1)\n if reg in AARCH64_CALLEE_SAVED or reg in AARCH64_CALLER_SAVED:\n spills.append((lineno, reg, line.strip()))\n elif re.match(r\"^q\\d+$\", reg):\n # q registers are the 128-bit view of v registers; q8–q15 are\n # partially callee-saved (lower 64 bits). For simplicity,\n # classify all q-register spills as caller-saved (I31).\n spills.append((lineno, reg, line.strip()))\n\n # Pair store: stp xN, xM / qN, qM, [sp, ...]\n m2 = RE_A64_STP_SPILL.search(line)\n if m2:\n for reg in (m2.group(1), m2.group(2)):\n if reg == \"xzr\":\n continue # zero register — this is a zero-store, not a spill\n if (\n reg in AARCH64_CALLEE_SAVED\n or reg in AARCH64_CALLER_SAVED\n or re.match(r\"^q\\d+$\", reg)\n ):\n spills.append((lineno, reg, line.strip()))\n\n findings: list[dict] = []\n seen: set[str] = set()\n for lineno, reg, line_text in spills:\n if reg not in seen:\n seen.add(reg)\n if reg in AARCH64_CALLEE_SAVED:\n reg_class, severity = \"callee-saved\", \"high\"\n elif (m := re.match(r\"^q(\\d+)$\", reg)) and int(m.group(1)) in range(8, 16):\n # q8–q15: lower 64 bits callee-saved per AAPCS64\n reg_class, severity = \"callee-saved (partial)\", \"high\"\n else:\n reg_class, severity = \"caller-saved\", \"medium\"\n findings.append(\n {\n \"category\": \"REGISTER_SPILL\",\n \"severity\": severity,\n \"symbol\": func_name,\n \"detail\": (\n f\"[EXPERIMENTAL] AArch64 register {reg} ({reg_class}) spilled to \"\n f\"stack at line {lineno} in function '{func_name}' \"\n f\"— may expose secret value\"\n ),\n \"evidence_detail\": f\"{line_text} at line {lineno}\",\n }\n )\n return findings\n\n\n# ---------------------------------------------------------------------------\n# Public entry point\n# ---------------------------------------------------------------------------\n\n\ndef analyze_function(\n func_name: str,\n func_lines: list[tuple[int, str]],\n) -> list[dict]:\n \"\"\"\n Run all AArch64 checks for one sensitive function.\n Returns a (possibly empty) list of finding dicts.\n\n [EXPERIMENTAL] All returned findings carry [EXPERIMENTAL] in their detail\n field and require manual verification.\n \"\"\"\n findings: list[dict] = []\n\n f = check_stack_retention(func_name, func_lines)\n if f:\n findings.append(f)\n\n findings.extend(check_register_spill(func_name, func_lines))\n\n return findings\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10246,"content_sha256":"0d63782b2b2f1a0e632451823d6001e4c8b240844a23c45c1e646602698e8db4"},{"filename":"tools/scripts/check_rust_asm_x86.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\ncheck_rust_asm_x86.py — x86-64 Rust assembly analysis backend.\n\nCalled by check_rust_asm.py. Not intended for direct invocation.\n\nDetects STACK_RETENTION, REGISTER_SPILL, and red-zone STACK_RETENTION in x86-64\nAT&T-syntax assembly emitted by `cargo +nightly rustc --emit=asm`.\n\"\"\"\n\nimport re\n\n# ---------------------------------------------------------------------------\n# x86-64 register sets (System V ABI — identical for C/C++ and Rust)\n# ---------------------------------------------------------------------------\nCALLER_SAVED = {\n \"rax\",\n \"rcx\",\n \"rdx\",\n \"rsi\",\n \"rdi\",\n \"r8\",\n \"r9\",\n \"r10\",\n \"r11\",\n # xmm0-xmm7 are function arguments / scratch; xmm8-xmm15 are also caller-saved\n # (System V AMD64 ABI §3.2.1: XMM registers 0–15 are all caller-saved)\n \"xmm0\",\n \"xmm1\",\n \"xmm2\",\n \"xmm3\",\n \"xmm4\",\n \"xmm5\",\n \"xmm6\",\n \"xmm7\",\n \"xmm8\",\n \"xmm9\",\n \"xmm10\",\n \"xmm11\",\n \"xmm12\",\n \"xmm13\",\n \"xmm14\",\n \"xmm15\",\n}\nCALLEE_SAVED = {\"rbx\", \"r12\", \"r13\", \"r14\", \"r15\", \"rbp\"}\n\n# ---------------------------------------------------------------------------\n# Patterns\n# ---------------------------------------------------------------------------\n\n# Frame allocation\nRE_FRAME_ALLOC = re.compile(r\"subq\\s+\\$(\\d+),\\s+%rsp\")\nRE_PUSH = re.compile(r\"push[ql]\\s+%(\\w+)\")\n\n# Zero-store patterns (volatile wipe) — all widths that can clear secret bytes\nRE_MOVQ_ZERO = re.compile(r\"movq\\s+\\$0,\\s+-?\\d+\\(%r[sb]p\\)\")\nRE_MOVL_ZERO = re.compile(r\"movl\\s+\\$0,\\s+-?\\d+\\(%r[sb]p\\)\")\nRE_MOVW_ZERO = re.compile(r\"movw\\s+\\$0,\\s+-?\\d+\\(%r[sb]p\\)\")\nRE_MOVB_ZERO = re.compile(r\"movb\\s+\\$0,\\s+-?\\d+\\(%r[sb]p\\)\")\nRE_MEMSET_CALL = re.compile(r\"call\\s+.*(?:memset|volatile_set_memory|zeroize)\")\n# SIMD self-XOR zeroing: xorps/pxor/vpxor %regN, %regN — register is zeroed,\n# typically followed by a store that constitutes the actual wipe.\nRE_SIMD_ZERO = re.compile(r\"(?:xorps|xorpd|pxor|vpxor)\\s+%(\\w+),\\s+%(\\w+)\")\n\n# Register spills: movq/movdqa/movups/movaps %reg, N(%rsp|%rbp)\nRE_REG_SPILL = re.compile(r\"mov(?:q|dqa|ups|aps)\\s+%(\\w+),\\s+(-?\\d+)\\(%r[sb]p\\)\")\n\n# Return instruction. Stripping the AT&T comment character (#) before\n# applying this pattern prevents false matches inside assembly comments\n# (e.g. \"# retq is the encoding for ...\").\nRE_RET = re.compile(r\"\\bret[ql]?\\b\")\n\n# Red zone: stores to [rsp - N] (N ≤ 128) in leaf functions without subq\nRE_RED_ZONE = re.compile(r\"mov(?:q|l|b|w)\\s+%\\w+,\\s+-(\\d+)\\(%rsp\\)\")\n\n\n# ---------------------------------------------------------------------------\n# STACK_RETENTION\n# ---------------------------------------------------------------------------\n\n\ndef check_stack_retention(\n func_name: str,\n func_lines: list[tuple[int, str]],\n) -> dict | None:\n \"\"\"\n Detect stack frame allocated (subq $N, %rsp) but not zeroed before return.\n \"\"\"\n frame_alloc_line: tuple[int, str] | None = None\n frame_size = 0\n has_zero_store = False\n ret_line: tuple[int, str] | None = None\n\n for lineno, line in func_lines:\n # Strip trailing AT&T-style comments before pattern matching to avoid\n # false positives from `# retq` or `# movq $0, ...` in comments (I25).\n code = line.split(\"#\", 1)[0]\n\n m = RE_FRAME_ALLOC.search(code)\n if m and frame_alloc_line is None:\n frame_alloc_line = (lineno, line.strip())\n frame_size = int(m.group(1))\n\n if (\n RE_MOVQ_ZERO.search(code)\n or RE_MOVL_ZERO.search(code)\n or RE_MOVW_ZERO.search(code)\n or RE_MOVB_ZERO.search(code)\n ):\n has_zero_store = True\n if RE_MEMSET_CALL.search(code):\n has_zero_store = True\n # SIMD self-XOR (xorps/pxor %xmmN, %xmmN) zeroes a register; treat\n # as a zero-store signal to avoid false-positive STACK_RETENTION when\n # the function wipes data via SIMD before returning (I26).\n m2 = RE_SIMD_ZERO.search(code)\n if m2 and m2.group(1) == m2.group(2):\n has_zero_store = True\n\n if RE_RET.search(code):\n ret_line = (lineno, line.strip())\n\n if frame_alloc_line and ret_line and not has_zero_store and frame_size > 0:\n alloc_lineno, alloc_text = frame_alloc_line\n ret_lineno, _ = ret_line\n return {\n \"category\": \"STACK_RETENTION\",\n \"severity\": \"high\",\n \"symbol\": func_name,\n \"detail\": (\n f\"Stack frame of {frame_size} bytes allocated at line {alloc_lineno} \"\n f\"({alloc_text!r}) but no zero-store found before return at line {ret_lineno}\"\n ),\n \"evidence_detail\": (\n f\"{alloc_text} at line {alloc_lineno}; \"\n f\"no volatile wipe before retq at line {ret_lineno}\"\n ),\n }\n return None\n\n\n# ---------------------------------------------------------------------------\n# REGISTER_SPILL\n# ---------------------------------------------------------------------------\n\n\ndef check_register_spill(\n func_name: str,\n func_lines: list[tuple[int, str]],\n) -> list[dict]:\n \"\"\"\n Detect registers spilled to the stack (potential secret exposure).\n \"\"\"\n spills: list[tuple[int, str, str, str]] = [] # (lineno, reg, line, class)\n\n for lineno, line in func_lines:\n m = RE_REG_SPILL.search(line)\n if m:\n reg = m.group(1)\n if reg in CALLER_SAVED:\n spills.append((lineno, reg, line.strip(), \"caller-saved\"))\n elif reg in CALLEE_SAVED:\n spills.append((lineno, reg, line.strip(), \"callee-saved\"))\n\n findings = []\n seen: set[str] = set()\n for lineno, reg, line_text, reg_class in spills:\n if reg not in seen:\n seen.add(reg)\n severity = \"high\" if reg_class == \"callee-saved\" else \"medium\"\n findings.append(\n {\n \"category\": \"REGISTER_SPILL\",\n \"severity\": severity,\n \"symbol\": func_name,\n \"detail\": (\n f\"Register %{reg} ({reg_class}) spilled to stack at line {lineno} \"\n f\"in function '{func_name}' — may expose secret value\"\n ),\n \"evidence_detail\": f\"{line_text} at line {lineno}\",\n }\n )\n return findings\n\n\n# ---------------------------------------------------------------------------\n# RED ZONE (x86-64 specific)\n# ---------------------------------------------------------------------------\n\n\ndef check_red_zone(\n func_name: str,\n func_lines: list[tuple[int, str]],\n) -> dict | None:\n \"\"\"\n Detect x86-64 leaf functions that store data in the red zone without zeroing.\n\n The x86-64 System V ABI reserves 128 bytes below %rsp as a \"red zone\" that\n leaf functions may use as scratch space without adjusting %rsp. Sensitive data\n written to this region is NOT zeroed by the callee and persists after return.\n This check only fires when no subq frame allocation is present (non-leaf\n functions are covered by check_stack_retention).\n \"\"\"\n # Only applies to leaf functions (no regular frame allocation)\n if any(RE_FRAME_ALLOC.search(line) for _, line in func_lines):\n return None\n\n red_zone_depth = 0\n has_zero_store = False\n has_ret = False\n\n for _, line in func_lines:\n code = line.split(\"#\", 1)[0] # strip AT&T comments (I25)\n\n m = RE_RED_ZONE.search(code)\n if m:\n offset = int(m.group(1))\n if offset \u003c= 128:\n red_zone_depth = max(red_zone_depth, offset)\n\n if (\n RE_MOVQ_ZERO.search(code)\n or RE_MOVL_ZERO.search(code)\n or RE_MOVW_ZERO.search(code)\n or RE_MOVB_ZERO.search(code)\n ):\n has_zero_store = True\n if RE_MEMSET_CALL.search(code):\n has_zero_store = True\n m2 = RE_SIMD_ZERO.search(code)\n if m2 and m2.group(1) == m2.group(2):\n has_zero_store = True\n if RE_RET.search(code):\n has_ret = True\n\n if red_zone_depth > 0 and has_ret and not has_zero_store:\n return {\n \"category\": \"STACK_RETENTION\",\n \"severity\": \"high\",\n \"symbol\": func_name,\n \"detail\": (\n f\"Leaf function '{func_name}' stores {red_zone_depth} bytes in the \"\n f\"x86-64 red zone (below %rsp) without zeroing before return — \"\n f\"sensitive data may persist in the 128-byte region below %rsp\"\n ),\n \"evidence_detail\": (\n f\"red zone depth -{red_zone_depth}(%rsp); \"\n f\"no mov[qwlb] $0 or memset/zeroize call before retq\"\n ),\n }\n return None\n\n\n# ---------------------------------------------------------------------------\n# Public entry point\n# ---------------------------------------------------------------------------\n\n\ndef analyze_function(\n func_name: str,\n func_lines: list[tuple[int, str]],\n) -> list[dict]:\n \"\"\"\n Run all x86-64 checks for one sensitive function.\n Returns a (possibly empty) list of finding dicts.\n \"\"\"\n findings: list[dict] = []\n\n f = check_stack_retention(func_name, func_lines)\n if f:\n findings.append(f)\n\n findings.extend(check_register_spill(func_name, func_lines))\n\n f = check_red_zone(func_name, func_lines)\n if f:\n findings.append(f)\n\n return findings\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9587,"content_sha256":"6edcdc741d1f202566c5914b5d577c879d11ba61a841ba42398e05579b919cea"},{"filename":"tools/scripts/check_rust_asm.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\ncheck_rust_asm.py — Rust assembly analysis dispatcher for STACK_RETENTION and REGISTER_SPILL.\n\nDetects the assembly architecture and delegates to the appropriate backend:\n x86-64 → check_rust_asm_x86.py (production-ready)\n AArch64 → check_rust_asm_aarch64.py (EXPERIMENTAL — findings require manual review)\n\nUsage:\n uv run check_rust_asm.py --asm \u003chash>.O2.s \\\\\n --secrets sensitive-objects.json \\\\\n --out asm-findings.json\n\"\"\"\n\nimport argparse\nimport importlib.util\nimport json\nimport re\nimport subprocess\nimport sys\nfrom collections import defaultdict\nfrom pathlib import Path\n\n# ---------------------------------------------------------------------------\n# Architecture detection\n# ---------------------------------------------------------------------------\n\n\ndef detect_architecture(asm_text: str) -> str:\n \"\"\"\n Heuristic architecture detection from assembly text.\n\n x86-64: AT&T percent-prefix 64-bit register names (%rsp, %rax, …)\n AArch64: distinctive ARM GNU syntax patterns (stp x29, str xzr, movi v#.*)\n \"\"\"\n # x86-64: AT&T percent-prefix 64-bit register names\n if re.search(r\"%r(?:sp|bp|ax|bx|cx|dx|si|di)\\b\", asm_text):\n return \"x86_64\"\n # AArch64: distinctive prologue / zero-register / SIMD instructions (ARM GNU syntax)\n if re.search(r\"stp\\s+x29|str\\s+xzr|stp\\s+xzr|movi\\s+v\\d+\\.\\w+\", asm_text):\n return \"aarch64\"\n # Broad AArch64 fallback: bare xN registers used as instruction operands\n if re.search(r\"\\b(?:x1[0-9]|x2[0-9]|x[0-9]),\", asm_text):\n return \"aarch64\"\n return \"unknown\"\n\n\n# ---------------------------------------------------------------------------\n# Symbol demangling (shared)\n# ---------------------------------------------------------------------------\n\n\ndef demangle_symbols(asm_text: str) -> str:\n \"\"\"Demangle all Rust symbols using rustfilt if available.\"\"\"\n try:\n result = subprocess.run(\n [\"rustfilt\"],\n input=asm_text,\n capture_output=True,\n text=True,\n timeout=30,\n )\n if result.returncode == 0:\n return result.stdout\n except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as e:\n msg = f\"rustfilt unavailable ({type(e).__name__})\"\n print(\n f\"[check_rust_asm] WARNING: {msg}, using regex demangling\",\n file=sys.stderr,\n )\n\n # Fallback: partial demangle via regex (strips hash suffix).\n # NOTE: The pattern _ZN[A-Za-z0-9_$]+E matches any Itanium-mangled symbol\n # (C++ included); it may garble non-Rust symbols into odd-looking paths.\n # This is cosmetic — the demangled text is only used for display purposes.\n # e.g. _ZN7example9SecretKey4wipe17h1a2b3c4d5e6f7g8hE -> example::SecretKey::wipe\n def _partial(m: re.Match) -> str:\n sym = m.group(0)\n inner = re.sub(r\"17h[0-9a-f]{16}E$\", \"\", sym)\n inner = re.sub(r\"^_ZN\", \"\", inner)\n parts = []\n while inner:\n num = re.match(r\"^(\\d+)\", inner)\n if not num:\n break\n n = int(num.group(1))\n inner = inner[len(num.group(1)) :]\n parts.append(inner[:n])\n inner = inner[n:]\n return \"::\".join(parts) if parts else sym\n\n return re.sub(r\"_ZN[A-Za-z0-9_$]+E\", _partial, asm_text)\n\n\n# ---------------------------------------------------------------------------\n# Assembly parsing (shared)\n# ---------------------------------------------------------------------------\n\nRE_FUNC_TYPE = re.compile(r\"\\.type\\s+(\\S+),\\s*@function\")\nRE_GLOBL = re.compile(r\"\\.globl\\s+(\\S+)\")\nRE_LABEL = re.compile(r\"^([A-Za-z_\\$][A-Za-z0-9_\\$@.]*):\")\n# Internal compiler-generated labels: Ltmp0, LBB0_1, .Ltmp0, etc.\nRE_INTERNAL_LABEL = re.compile(r\"^\\.?L[A-Z_]\")\n\n\ndef parse_functions(asm_lines: list[str]) -> dict[str, list[tuple[int, str]]]:\n \"\"\"\n Split assembly into per-function sections.\n Returns {function_name: [(line_no, line_text), ...]}\n\n Supports both ELF (`.type sym,@function`) and Mach-O (`.globl sym`)\n object formats. When no `.type` directives are found (macOS), falls back\n to `.globl` symbols. Internal compiler labels (LBB0_1, Ltmp0, .Ltmp0)\n are always excluded from function-start candidates.\n \"\"\"\n functions: dict[str, list[tuple[int, str]]] = {}\n current: str | None = None\n current_lines: list[tuple[int, str]] = []\n\n func_names: set[str] = set()\n for line in asm_lines:\n m = RE_FUNC_TYPE.search(line)\n if m:\n func_names.add(m.group(1))\n\n # Mach-O fallback: if no ELF .type directives found, use .globl symbols\n if not func_names:\n for line in asm_lines:\n m = RE_GLOBL.search(line)\n if m:\n func_names.add(m.group(1))\n\n for lineno, line in enumerate(asm_lines, 1):\n stripped = line.strip()\n m = RE_LABEL.match(stripped)\n if m:\n label = m.group(1)\n # Always skip internal compiler-generated labels regardless of func_names\n if RE_INTERNAL_LABEL.match(label):\n if current is not None:\n current_lines.append((lineno, line))\n continue\n if not func_names or label in func_names:\n if current is not None:\n functions[current] = current_lines\n current = label\n current_lines = [(lineno, line)]\n continue\n if current is not None:\n current_lines.append((lineno, line))\n\n if current is not None:\n functions[current] = current_lines\n\n return functions\n\n\n# ---------------------------------------------------------------------------\n# Sensitive object matching (shared)\n# ---------------------------------------------------------------------------\n\n\ndef load_secrets(secrets_path: str) -> list[str] | None:\n \"\"\"Return sensitive type/symbol names from sensitive-objects.json.\n\n Returns an empty list when the file is absent (no secrets configured is\n valid), or None when the file exists but contains corrupt JSON (signals an\n error to the caller so analysis is not silently skipped).\n \"\"\"\n try:\n with open(secrets_path, encoding=\"utf-8\") as f:\n objects = json.load(f)\n names = []\n for obj in objects:\n if obj.get(\"language\") == \"rust\":\n names.append(obj.get(\"name\", \"\"))\n return [n for n in names if n]\n except FileNotFoundError:\n return []\n except json.JSONDecodeError as e:\n print(\n f\"[check_rust_asm] ERROR: corrupt secrets JSON at {secrets_path!r}: {e}\",\n file=sys.stderr,\n )\n return None\n\n\ndef is_sensitive_function(func_name: str, sensitive_names: list[str]) -> bool:\n \"\"\"True if the demangled function name relates to a sensitive type.\"\"\"\n lower = func_name.lower()\n if \"drop_in_place\" in lower:\n return any(name.lower() in lower for name in sensitive_names)\n return any(name.lower() in lower for name in sensitive_names)\n\n\n# ---------------------------------------------------------------------------\n# Drop glue check (shared — covers both x86-64 `call` and AArch64 `bl`)\n# ---------------------------------------------------------------------------\n\n# Matches both x86-64 `call` and AArch64 `bl` to zeroize/memset routines\nRE_WIPE_CALL = re.compile(r\"(?:call|bl)\\s+.*(?:memset|volatile_set_memory|zeroize)\")\n\n\ndef check_drop_glue(\n func_name: str,\n func_lines: list[tuple[int, str]],\n) -> dict | None:\n \"\"\"\n For drop_in_place::\u003cSensitiveType> functions, check for zeroize calls.\n If absent, emit MISSING_SOURCE_ZEROIZE (medium) as corroboration.\n Works for both x86-64 and AArch64 assembly.\n \"\"\"\n if \"drop_in_place\" not in func_name.lower():\n return None\n\n has_zeroize = any(\n RE_WIPE_CALL.search(line) or \"zeroize\" in line.lower() for _, line in func_lines\n )\n if not has_zeroize:\n return {\n \"category\": \"MISSING_SOURCE_ZEROIZE\",\n \"severity\": \"medium\",\n \"symbol\": func_name,\n \"detail\": (\n f\"drop_in_place for '{func_name}' has no zeroize/volatile-store calls \"\n f\"— sensitive type may not be wiped on drop\"\n ),\n \"evidence_detail\": (\n f\"No zeroize call found in {func_name} drop glue ({len(func_lines)} lines)\"\n ),\n }\n return None\n\n\n# ---------------------------------------------------------------------------\n# Arch module loader\n# ---------------------------------------------------------------------------\n\n\ndef _load_arch_module(name: str):\n \"\"\"Load an arch backend module from the same directory as this script.\"\"\"\n script_dir = Path(__file__).parent\n module_path = script_dir / f\"{name}.py\"\n spec = importlib.util.spec_from_file_location(name, module_path)\n if spec is None or spec.loader is None:\n raise ImportError(\n f\"Cannot load arch module {name!r} from {module_path} — \"\n \"file not found or not a valid Python module\"\n )\n module = importlib.util.module_from_spec(spec)\n spec.loader.exec_module(module)\n return module\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\n\ndef main() -> int:\n parser = argparse.ArgumentParser(\n description=\"Analyze Rust assembly for STACK_RETENTION and REGISTER_SPILL\"\n )\n parser.add_argument(\"--asm\", required=True, help=\"Path to .s assembly file\")\n parser.add_argument(\"--secrets\", required=True, help=\"Path to sensitive-objects.json\")\n parser.add_argument(\"--out\", required=True, help=\"Output JSON path\")\n args = parser.parse_args()\n\n out_path = Path(args.out)\n\n def _write_empty_and_return(code: int, message: str = \"\") -> int:\n out_path.parent.mkdir(parents=True, exist_ok=True)\n if code != 0 and message:\n error_output = [\n {\n \"id\": \"F-RUST-ASM-ERROR\",\n \"category\": \"ANALYSIS_ERROR\",\n \"severity\": \"info\",\n \"detail\": message,\n \"location\": {\"file\": str(asm_path), \"line\": 0},\n }\n ]\n out_path.write_text(json.dumps(error_output, indent=2), encoding=\"utf-8\")\n else:\n out_path.write_text(\"[]\", encoding=\"utf-8\")\n return code\n\n asm_path = Path(args.asm)\n if not asm_path.exists():\n print(f\"[check_rust_asm] ERROR: assembly file not found: {asm_path}\", file=sys.stderr)\n return _write_empty_and_return(1, f\"Assembly file not found: {asm_path}\")\n\n try:\n asm_text = asm_path.read_text(encoding=\"utf-8\", errors=\"replace\")\n except OSError as e:\n print(f\"[check_rust_asm] ERROR: cannot read assembly file: {e}\", file=sys.stderr)\n return _write_empty_and_return(1, f\"Cannot read assembly file: {e}\")\n\n arch = detect_architecture(asm_text)\n\n if arch == \"x86_64\":\n try:\n arch_module = _load_arch_module(\"check_rust_asm_x86\")\n except ImportError as e:\n print(f\"[check_rust_asm] ERROR: cannot load x86 backend: {e}\", file=sys.stderr)\n return _write_empty_and_return(1, f\"Cannot load x86 backend: {e}\")\n elif arch == \"aarch64\":\n print(\n \"[check_rust_asm] NOTE: AArch64 support is EXPERIMENTAL. \"\n \"Findings require manual verification before inclusion in a report.\",\n file=sys.stderr,\n )\n try:\n arch_module = _load_arch_module(\"check_rust_asm_aarch64\")\n except ImportError as e:\n print(f\"[check_rust_asm] ERROR: cannot load AArch64 backend: {e}\", file=sys.stderr)\n return _write_empty_and_return(1, f\"Cannot load AArch64 backend: {e}\")\n else:\n print(\n f\"[check_rust_asm] WARNING: unsupported assembly architecture '{arch}'. \"\n \"Writing skipped finding.\",\n file=sys.stderr,\n )\n output = [\n {\n \"id\": \"F-RUST-ASM-SKIP-0001\",\n \"category\": \"ANALYSIS_SKIPPED\",\n \"severity\": \"info\",\n \"confidence\": \"confirmed\",\n \"detail\": f\"Unsupported assembly architecture '{arch}' -- no analysis performed\",\n \"location\": {\"file\": str(asm_path), \"line\": 0},\n }\n ]\n out_path.parent.mkdir(parents=True, exist_ok=True)\n out_path.write_text(json.dumps(output, indent=2), encoding=\"utf-8\")\n return 0\n\n asm_demangled = demangle_symbols(asm_text)\n asm_lines = asm_demangled.splitlines(keepends=True)\n\n sensitive_names = load_secrets(args.secrets)\n if sensitive_names is None:\n print(\"[check_rust_asm] ERROR: aborting due to corrupt secrets file\", file=sys.stderr)\n return _write_empty_and_return(1, \"Aborting due to corrupt secrets file\")\n if not sensitive_names:\n print(\n \"[check_rust_asm] WARNING: no Rust sensitive objects found in secrets file\",\n file=sys.stderr,\n )\n\n functions = parse_functions([line.rstrip(\"\\n\") for line in asm_lines])\n\n # Deduplicate: collapse monomorphized instances of the same generic function.\n seen_findings: dict[tuple, dict] = {}\n instance_counts: dict[tuple, int] = defaultdict(int)\n raw_findings: list[dict] = []\n\n def _dedup_key(finding: dict, base_name: str) -> tuple:\n if finding[\"category\"] == \"REGISTER_SPILL\":\n return (finding[\"category\"], base_name, finding.get(\"evidence_detail\", \"\"))\n return (finding[\"category\"], base_name)\n\n def _record(finding: dict, base_name: str) -> None:\n key = _dedup_key(finding, base_name)\n instance_counts[key] += 1\n if key not in seen_findings:\n seen_findings[key] = finding\n # Store base_name so the output phase can reconstruct the dedup key\n # without recomputing it from finding[\"symbol\"] (which may differ due\n # to monomorphization hash stripping vs. type-param stripping).\n finding[\"_base_name\"] = base_name\n raw_findings.append(finding)\n\n for func_name, func_lines in functions.items():\n if not is_sensitive_function(func_name, sensitive_names):\n continue\n\n # Derive base name: strip monomorphization hash and type params\n base_name = re.sub(r\"::h[0-9a-f]{16}$\", \"\", func_name)\n base_name = re.sub(r\"::\u003c[^>]+>\", \"\", base_name)\n\n # Arch-specific findings (STACK_RETENTION, REGISTER_SPILL, red zone)\n for finding in arch_module.analyze_function(func_name, func_lines):\n _record(finding, base_name)\n\n # Drop glue check (shared — works for both x86-64 and AArch64)\n finding = check_drop_glue(func_name, func_lines)\n if finding:\n _record(finding, base_name)\n\n # Assign IDs and build final output\n output = []\n for idx, finding in enumerate(raw_findings, 1):\n base_name = finding.pop(\"_base_name\", finding[\"symbol\"])\n key = _dedup_key(finding, base_name)\n count = instance_counts.get(key, 1)\n evidence_detail = finding.pop(\"evidence_detail\", \"\")\n if count > 1:\n evidence_detail += f\" (seen in {count} monomorphized instances)\"\n output.append(\n {\n \"id\": f\"F-RUST-ASM-{idx:04d}\",\n \"language\": \"rust\",\n \"category\": finding[\"category\"],\n \"severity\": finding[\"severity\"],\n \"symbol\": finding[\"symbol\"],\n \"detail\": finding[\"detail\"],\n \"evidence\": [{\"source\": \"asm\", \"detail\": evidence_detail}],\n \"evidence_files\": [str(asm_path)],\n }\n )\n\n out_path.parent.mkdir(parents=True, exist_ok=True)\n out_path.write_text(json.dumps(output, indent=2), encoding=\"utf-8\")\n print(f\"[check_rust_asm] {len(output)} finding(s) written to {args.out}\", file=sys.stderr)\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":16243,"content_sha256":"3c82bdb8db9c4393949cc437ff3ade8da3a610c27ebbfb54501132f58ad6d675"},{"filename":"tools/scripts/find_dangerous_apis.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\nfind_dangerous_apis.py — Token/grep-based scanner for dangerous Rust API patterns.\n\nScans .rs files for API calls that bypass zeroization guarantees (mem::forget,\nBox::leak, ptr::write_bytes, etc.) and async suspension points that expose\nsecret-named locals to the heap-allocated Future state machine.\n\nDoes NOT require compilation — pure source text analysis.\n\nUsage:\n uv run find_dangerous_apis.py --src \u003csource_dir> --out \u003cfindings.json>\n\nExit codes:\n 0 — ran successfully (findings may be empty)\n 1 — source directory not found\n 2 — argument error\n\"\"\"\n\nimport argparse\nimport json\nimport re\nimport sys\nfrom pathlib import Path\n\n# ---------------------------------------------------------------------------\n# Sensitive name patterns (used for context filtering)\n# ---------------------------------------------------------------------------\n\nSENSITIVE_NAME_RE = re.compile(\n # PascalCase type names use \\b (no underscore in names like SecretKey).\n # Lowercase keywords use (?\u003c![a-zA-Z])...(?![a-zA-Z]) so that snake_case\n # names like 'secret_key', 'private_key', and 'auth_token' are matched\n # while avoiding spurious hits on words like 'monkey' or 'tokenize'.\n r\"(?i)(?:\\b(Key|PrivateKey|SecretKey|SigningKey|MasterKey|HmacKey|\"\n r\"Password|Passphrase|Pin|Token|AuthToken|BearerToken|ApiKey|\"\n r\"Secret|SharedSecret|PreSharedKey|Nonce|Seed|Entropy|\"\n r\"Credential|SessionKey|DerivedKey)\\b\"\n r\"|(?\u003c![a-zA-Z])(key|secret|password|token|nonce|seed|private|master|credential)(?![a-zA-Z]))\"\n)\n\n# ---------------------------------------------------------------------------\n# Dangerous API patterns: (regex, category, severity, detail)\n# ---------------------------------------------------------------------------\n\nPATTERNS: list[tuple[str, str, str, str]] = [\n (\n r\"\\bmem::forget\\s*\\(\",\n \"MISSING_SOURCE_ZEROIZE\",\n \"critical\",\n \"mem::forget() prevents Drop/ZeroizeOnDrop from running — secret never wiped\",\n ),\n (\n r\"\\bManuallyDrop\\s*::\\s*new\\s*\\(\",\n \"MISSING_SOURCE_ZEROIZE\",\n \"critical\",\n \"ManuallyDrop::new() suppresses automatic drop — \"\n \"secret not wiped unless drop() called explicitly\",\n ),\n (\n r\"\\bBox\\s*::\\s*leak\\s*\\(\",\n \"MISSING_SOURCE_ZEROIZE\",\n \"critical\",\n \"Box::leak() — leaked allocation is never dropped or zeroed\",\n ),\n (\n r\"\\bBox\\s*::\\s*into_raw\\s*\\(\",\n \"MISSING_SOURCE_ZEROIZE\",\n \"high\",\n \"Box::into_raw() — raw pointer escapes Drop; \"\n \"must call Box::from_raw() + zeroize to reclaim\",\n ),\n (\n r\"\\bptr\\s*::\\s*write_bytes\\s*\\(\",\n \"OPTIMIZED_AWAY_ZEROIZE\",\n \"high\",\n \"ptr::write_bytes() is non-volatile — LLVM may eliminate as dead store. \"\n \"Use zeroize crate or add compiler_fence(SeqCst) after\",\n ),\n (\n # Matches both turbofish form (transmute::\u003cT, U>(v)) and type-inferred form (transmute(v))\n r\"\\bmem\\s*::\\s*transmute\\b\",\n \"SECRET_COPY\",\n \"high\",\n \"mem::transmute creates a bitwise copy — original and transmuted value both exist on stack\",\n ),\n (\n r\"\\bslice\\s*::\\s*from_raw_parts\\s*\\(\",\n \"SECRET_COPY\",\n \"medium\",\n \"slice::from_raw_parts creates a slice alias over raw memory — may alias a secret buffer\",\n ),\n (\n r\"\\bmem\\s*::\\s*take\\s*\\(\",\n \"MISSING_SOURCE_ZEROIZE\",\n \"medium\",\n \"mem::take() replaces the value in-place without zeroing the original location\",\n ),\n (\n r\"\\bmem\\s*::\\s*uninitialized\\s*\\(\",\n \"MISSING_SOURCE_ZEROIZE\",\n \"critical\",\n \"mem::uninitialized() is deprecated and unsafe — \"\n \"may expose prior secret bytes from stack memory\",\n ),\n]\n\n# Pre-compile all pattern regexes at module load time (avoids recompiling per file).\n_COMPILED_PATTERNS: list[tuple[re.Pattern, str, str, str]] = [\n (re.compile(pattern), category, severity, detail)\n for pattern, category, severity, detail in PATTERNS\n]\n\n# ---------------------------------------------------------------------------\n# Finding counter\n# ---------------------------------------------------------------------------\n\n_finding_counter = [0]\n\n\ndef make_finding(\n category: str,\n severity: str,\n detail: str,\n file: str,\n line: int,\n symbol: str = \"\",\n confidence: str = \"likely\",\n) -> dict:\n _finding_counter[0] += 1\n fid = f\"F-RUST-SRC-{_finding_counter[0]:04d}\"\n return {\n \"id\": fid,\n \"language\": \"rust\",\n \"category\": category,\n \"severity\": severity,\n \"confidence\": confidence,\n \"detail\": detail,\n \"symbol\": symbol,\n \"location\": {\"file\": file, \"line\": line},\n \"evidence\": [{\"source\": \"source_grep\", \"detail\": detail}],\n }\n\n\n# ---------------------------------------------------------------------------\n# Context sensitivity check\n# ---------------------------------------------------------------------------\n\n\ndef has_sensitive_context(lines: list[str], center_idx: int, window: int = 15) -> bool:\n \"\"\"Return True if any sensitive name appears within `window` lines of `center_idx`.\n\n `center_idx` is a 0-based array index (i.e. ``lineno - 1``). Callers must\n NOT pass 1-based line numbers here or the window will be off by one.\n \"\"\"\n start = max(0, center_idx - window)\n end = min(len(lines), center_idx + window + 1)\n context = \"\\n\".join(lines[start:end])\n return bool(SENSITIVE_NAME_RE.search(context))\n\n\n# ---------------------------------------------------------------------------\n# Grep-based pattern scanner\n# ---------------------------------------------------------------------------\n\n_BLOCK_COMMENT_START = re.compile(r\"/\\*\")\n_BLOCK_COMMENT_END = re.compile(r\"\\*/\")\n\n\ndef _is_commented_out(line: str, in_block_comment: bool) -> tuple[bool, bool]:\n \"\"\"Return (skip_this_line, updated_in_block_comment).\n\n Handles single-line `//` comments and block `/* ... */` comments. A line\n that merely *contains* a comment start (e.g. `foo(); /* note */`) is NOT\n fully skipped — only lines where the match site is inside the comment region\n would be skipped. For simplicity this implementation skips the entire line\n when it starts with `//` (after stripping) or when we are inside a block\n comment. This is intentionally conservative: it may miss a pattern on the\n same source line as an unrelated comment, but that is a very rare case.\n \"\"\"\n stripped = line.strip()\n if in_block_comment:\n if _BLOCK_COMMENT_END.search(line):\n return True, False # end of block comment on this line; skip line\n return True, True # still inside block comment\n if stripped.startswith(\"//\"):\n return True, False # single-line comment\n if stripped.startswith(\"/*\"):\n if _BLOCK_COMMENT_END.search(line):\n return True, False # block comment opens and closes on this line\n return True, True # block comment opens; skip remainder\n # Mid-line block comment: code precedes the /* (e.g. `code(); /* comment ...`).\n # Do not skip this line (the match site may be in the code portion), but mark\n # subsequent lines as inside a block comment.\n if _BLOCK_COMMENT_START.search(stripped) and not _BLOCK_COMMENT_END.search(stripped):\n return False, True\n return False, False\n\n\ndef scan_file_patterns(path: Path, source: str) -> list[dict]:\n findings: list[dict] = []\n lines = source.splitlines()\n in_block_comment = False\n\n for compiled, category, severity, detail in _COMPILED_PATTERNS:\n in_block_comment = False # reset per pattern pass\n for lineno, line in enumerate(lines, start=1):\n skip, in_block_comment = _is_commented_out(line, in_block_comment)\n if skip:\n continue\n if not compiled.search(line):\n continue\n actual_severity = severity\n actual_confidence = \"likely\"\n if not has_sensitive_context(lines, lineno - 1): # lineno-1 → 0-based\n actual_confidence = \"needs_review\"\n findings.append(\n make_finding(\n category,\n actual_severity,\n detail,\n str(path),\n lineno,\n confidence=actual_confidence,\n )\n )\n\n return findings\n\n\n# ---------------------------------------------------------------------------\n# Async secret suspension detector\n# ---------------------------------------------------------------------------\n\n\ndef scan_async_suspension(path: Path, source: str) -> list[dict]:\n \"\"\"\n Detect: async fn body where a secret-named local is bound before an .await.\n\n Heuristic:\n 1. Find async fn declarations.\n 2. Within each async fn body (between opening { and matching }), find let bindings\n whose variable name matches SENSITIVE_NAME_RE.\n 3. Check whether any .await appears after the binding within the same fn body.\n 4. If so, emit NOT_ON_ALL_PATHS (high).\n \"\"\"\n findings: list[dict] = []\n lines = source.splitlines()\n\n # Find all async fn start lines\n async_fn_re = re.compile(r\"\\basync\\s+fn\\s+\\w+\")\n let_binding_re = re.compile(r\"\\blet\\s+(?:mut\\s+)?(\\w+)\\s*[=:]\")\n await_re = re.compile(r\"\\.await\\b\")\n\n i = 0\n while i \u003c len(lines):\n if async_fn_re.search(lines[i]):\n # Find the body: scan for opening brace\n body_lines: list[tuple[int, str]] = []\n depth = 0\n in_body = False\n for j in range(i, min(i + 500, len(lines))):\n # Count braces, skipping string literals and line comments\n in_str = False\n k = 0\n line_text = lines[j]\n while k \u003c len(line_text):\n ch = line_text[k]\n if in_str:\n if ch == \"\\\\\" and k + 1 \u003c len(line_text):\n k += 2 # skip escape sequence\n continue\n elif ch == '\"':\n in_str = False\n else:\n if ch == '\"':\n in_str = True\n elif ch == \"/\" and k + 1 \u003c len(line_text) and line_text[k + 1] == \"/\":\n break # rest of line is a comment\n elif ch == \"{\":\n depth += 1\n in_body = True\n elif ch == \"}\":\n depth -= 1\n k += 1\n if in_body:\n body_lines.append((j + 1, lines[j])) # 1-based line number\n if in_body and depth == 0:\n i = j + 1\n break\n else:\n i += 1\n continue\n\n # Within body, find secret-named bindings followed by .await\n secret_bindings: list[tuple[int, str]] = [] # (lineno, varname)\n for lineno, line in body_lines:\n m = let_binding_re.search(line)\n if m and SENSITIVE_NAME_RE.search(m.group(1)):\n secret_bindings.append((lineno, m.group(1)))\n\n for bind_line, varname in secret_bindings:\n # Check if .await appears after this binding in the fn body\n for lineno, line in body_lines:\n if lineno > bind_line and await_re.search(line):\n findings.append(\n make_finding(\n \"NOT_ON_ALL_PATHS\",\n \"high\",\n f\"Secret local '{varname}' is live across an .await suspension \"\n \"point in an async fn — stored in the heap-allocated Future state \"\n \"machine; ZeroizeOnDrop covers stack variables only\",\n str(path),\n bind_line,\n )\n )\n break # one finding per binding is enough\n continue\n i += 1\n\n return findings\n\n\n# ---------------------------------------------------------------------------\n# Main scanner\n# ---------------------------------------------------------------------------\n\n\ndef scan_directory(src_dir: Path) -> list[dict]:\n findings: list[dict] = []\n for rs_file in sorted(src_dir.rglob(\"*.rs\")):\n try:\n source = rs_file.read_text(encoding=\"utf-8\", errors=\"replace\")\n except OSError as e:\n print(f\"find_dangerous_apis.py: warning: cannot read {rs_file}: {e}\", file=sys.stderr)\n continue\n findings.extend(scan_file_patterns(rs_file, source))\n findings.extend(scan_async_suspension(rs_file, source))\n return findings\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\n\ndef main() -> int:\n parser = argparse.ArgumentParser(\n description=\"Token/grep-based scanner for dangerous Rust API patterns\"\n )\n parser.add_argument(\"--src\", required=True, help=\"Source directory to scan (.rs files)\")\n parser.add_argument(\"--out\", required=True, help=\"Output findings JSON path\")\n args = parser.parse_args()\n\n src_dir = Path(args.src)\n if not src_dir.is_dir():\n print(f\"find_dangerous_apis.py: source directory not found: {src_dir}\", file=sys.stderr)\n return 1\n\n findings = scan_directory(src_dir)\n\n out_path = Path(args.out)\n out_path.parent.mkdir(parents=True, exist_ok=True)\n out_path.write_text(json.dumps(findings, indent=2), encoding=\"utf-8\")\n\n print(f\"find_dangerous_apis.py: {len(findings)} finding(s) written to {out_path}\")\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":14132,"content_sha256":"5a7b873e17a64bdbe0743a0301f1abd0aba4bbe79e552eccdf7dc39e80563469"},{"filename":"tools/scripts/semantic_audit.py","content":"#!/usr/bin/env python3\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = []\n# ///\n\"\"\"\nsemantic_audit.py — Rust trait-aware zeroization auditor.\n\nReads rustdoc JSON (generated by `cargo +nightly rustdoc --document-private-items\n-- -Z unstable-options --output-format json`) and emits findings about missing or\nincorrect zeroization of sensitive types.\n\nUsage:\n uv run semantic_audit.py --rustdoc \u003cpath.json> [--cargo-toml \u003cCargo.toml>] --out \u003cfindings.json>\n\nExit codes:\n 0 — ran successfully (findings may be empty)\n 1 — rustdoc JSON not found or unparseable\n 2 — argument error\n\"\"\"\n\nimport argparse\nimport json\nimport re\nimport sys\nimport tomllib\nfrom pathlib import Path\nfrom typing import Any\n\n# ---------------------------------------------------------------------------\n# Sensitive type / field name patterns\n# ---------------------------------------------------------------------------\n\nSENSITIVE_TYPE_RE = re.compile(\n r\"(?i)(Key|PrivateKey|SecretKey|SigningKey|MasterKey|HmacKey|\"\n r\"Password|Passphrase|Pin|Token|AuthToken|BearerToken|ApiKey|\"\n r\"Secret|SharedSecret|PreSharedKey|Nonce|Seed|Entropy|\"\n r\"Credential|SessionKey|DerivedKey)\"\n)\n\nSENSITIVE_FIELD_RE = re.compile(\n r\"(?i)\\b(key|secret|password|token|nonce|seed|private|master|credential)\\b\"\n)\n\n# Derives/traits that indicate zeroization intent\nZEROIZE_TRAITS = {\"Zeroize\", \"ZeroizeOnDrop\"}\nDROP_TRAIT = \"Drop\"\n\n# Traits / derives that create untracked copies\nCOPY_DERIVES = {\"Copy\"}\nCLONE_DERIVES = {\"Clone\"}\nDEBUG_DERIVES = {\"Debug\"}\nSERIALIZE_DERIVES = {\"Serialize\"}\n\n# Evidence tags used for conservative confidence mapping.\nSTRONG_EVIDENCE_TAGS = {\n \"trait_impl\",\n \"resolved_path\",\n \"drop_body_source\",\n \"cargo_toml\",\n}\n\nMEDIUM_EVIDENCE_TAGS = {\n \"source_scan\",\n \"generic_traversal\",\n}\n\nHEAP_TYPE_NAMES = {\n \"Vec\",\n \"Box\",\n \"String\",\n \"HashMap\",\n \"BTreeMap\",\n \"VecDeque\",\n \"BinaryHeap\",\n \"LinkedList\",\n}\n\nZEROIZING_WRAPPER_NAMES = {\n \"Zeroizing\",\n}\n\nMANUALLY_DROP_NAMES = {\"ManuallyDrop\"}\n\nZEROIZING_NAME_HINT_RE = re.compile(r\"(?i)(Zeroiz|Protected|Secret|Sensitive)\")\n\n\n# ---------------------------------------------------------------------------\n# Helper: is a type name sensitive?\n# ---------------------------------------------------------------------------\n\n\ndef is_sensitive_name(name: str) -> bool:\n return bool(SENSITIVE_TYPE_RE.search(name))\n\n\ndef has_sensitive_field(fields: list[dict]) -> bool:\n for field in fields:\n fname = field.get(\"name\") or \"\"\n if SENSITIVE_FIELD_RE.search(fname):\n return True\n return False\n\n\n# ---------------------------------------------------------------------------\n# Finding builder\n# ---------------------------------------------------------------------------\n\n_finding_counter = [0]\n\n\ndef make_finding(\n category: str,\n severity: str,\n detail: str,\n type_name: str,\n file: str,\n line: int | None,\n confidence: str | None = None,\n evidence_strength: list[str] | None = None,\n) -> dict:\n _finding_counter[0] += 1\n fid = f\"F-RUST-SRC-{_finding_counter[0]:04d}\"\n evidence_strength = evidence_strength or [\"heuristic\"]\n resolved_confidence = confidence or _confidence_from_evidence_strength(evidence_strength)\n return {\n \"id\": fid,\n \"language\": \"rust\",\n \"category\": category,\n \"severity\": severity,\n \"confidence\": resolved_confidence,\n \"evidence_strength\": evidence_strength,\n \"detail\": detail,\n \"symbol\": type_name,\n \"object\": {\"name\": type_name},\n \"location\": {\"file\": file, \"line\": line or 1},\n \"evidence\": [\n {\n \"source\": \"rustdoc_json\",\n \"detail\": detail,\n \"strength\": evidence_strength,\n }\n ],\n }\n\n\ndef _confidence_from_evidence_strength(evidence_strength: list[str]) -> str:\n strong_count = sum(1 for tag in evidence_strength if tag in STRONG_EVIDENCE_TAGS)\n medium_count = sum(1 for tag in evidence_strength if tag in MEDIUM_EVIDENCE_TAGS)\n if strong_count >= 2:\n return \"confirmed\"\n if strong_count == 1:\n return \"likely\"\n if medium_count >= 1 and not any(tag == \"heuristic\" for tag in evidence_strength):\n return \"likely\"\n return \"needs_review\"\n\n\n# ---------------------------------------------------------------------------\n# Rustdoc JSON helpers\n# ---------------------------------------------------------------------------\n\n\ndef item_span(item: dict) -> tuple[str, int | None]:\n \"\"\"Return (file, line) from an item's span.\"\"\"\n span = item.get(\"span\") or {}\n filename = span.get(\"filename\") or \"\"\n begin = span.get(\"begin\") or []\n line = begin[0] if begin else None\n return filename, line\n\n\ndef item_derives(item: dict) -> set[str]:\n \"\"\"Collect derive macro names from item attrs.\"\"\"\n derives: set[str] = set()\n for attr in item.get(\"attrs\") or []:\n # attr is a string like '#[derive(Copy, Clone, Debug)]'\n m = re.search(r\"derive\\(([^)]+)\\)\", attr)\n if m:\n for d in m.group(1).split(\",\"):\n derives.add(d.strip())\n return derives\n\n\ndef item_impls(item: dict, index: dict) -> set[str]:\n \"\"\"Return trait names implemented by this struct/enum via its impl IDs.\"\"\"\n trait_names: set[str] = set()\n for impl_id in item.get(\"impls\") or []:\n impl_item = index.get(str(impl_id)) or {}\n inner = impl_item.get(\"inner\") or {}\n impl_data = inner.get(\"impl\") or {}\n trait_ref = impl_data.get(\"trait\") or {}\n tname = _trait_name(trait_ref)\n if tname:\n trait_names.add(tname)\n return trait_names\n\n\ndef _trait_name(trait_ref: dict[str, Any]) -> str:\n name = trait_ref.get(\"name\")\n if isinstance(name, str) and name:\n return name.split(\"::\")[-1]\n resolved = trait_ref.get(\"resolved_path\")\n if isinstance(resolved, dict):\n resolved_name = resolved.get(\"name\")\n if isinstance(resolved_name, str) and resolved_name:\n return resolved_name.split(\"::\")[-1]\n return \"\"\n\n\ndef struct_fields(item: dict, index: dict) -> list[dict]:\n \"\"\"Return field items for a struct.\"\"\"\n fields: list[dict] = []\n inner = item.get(\"inner\") or {}\n struct_data = inner.get(\"struct\") or {}\n kind = struct_data.get(\"kind\") or {}\n # plain struct: kind = {\"plain\": {\"fields\": [id, ...], ...}}\n plain = kind.get(\"plain\") or {}\n field_ids = plain.get(\"fields\") or []\n for fid in field_ids:\n fitem = index.get(str(fid)) or {}\n fields.append(fitem)\n return fields\n\n\n# ---------------------------------------------------------------------------\n# Core analysis\n# ---------------------------------------------------------------------------\n\n\ndef analyze(rustdoc: dict, cargo_toml_path: str | None) -> list[dict]:\n findings: list[dict] = []\n index: dict = rustdoc.get(\"index\") or {}\n\n # Check whether zeroize crate is a dependency\n has_zeroize_dep = _check_zeroize_dep(cargo_toml_path)\n\n for _item_id, item in index.items():\n kind = item.get(\"kind\") or \"\"\n if kind not in (\"struct\", \"enum\"):\n continue\n\n name = item.get(\"name\") or \"\"\n if not is_sensitive_name(name):\n # Check fields too\n fields = struct_fields(item, index) if kind == \"struct\" else []\n if not has_sensitive_field(fields):\n continue\n\n file, line = item_span(item)\n derives = item_derives(item)\n trait_impls = item_impls(item, index)\n\n # --- 1. Copy derive on sensitive type ---\n if COPY_DERIVES & derives:\n findings.append(\n make_finding(\n \"SECRET_COPY\",\n \"critical\",\n f\"#[derive(Copy)] on sensitive type '{name}' — all assignments are \"\n \"untracked duplicates, no Drop ever runs\",\n name,\n file,\n line,\n evidence_strength=[\"attr_only\", \"sensitive_name_or_field\"],\n )\n )\n\n # --- 2. No Zeroize / ZeroizeOnDrop / Drop ---\n # (Skip for Copy types: Copy and Drop are mutually exclusive in Rust.)\n has_zeroize = bool(ZEROIZE_TRAITS & trait_impls)\n has_drop = DROP_TRAIT in trait_impls\n has_zeroize_on_drop = \"ZeroizeOnDrop\" in trait_impls or \"ZeroizeOnDrop\" in derives\n\n if not (COPY_DERIVES & derives):\n if not has_zeroize and not has_drop and not has_zeroize_on_drop:\n findings.append(\n make_finding(\n \"MISSING_SOURCE_ZEROIZE\",\n \"high\",\n f\"Sensitive type '{name}' has no Zeroize, ZeroizeOnDrop,\"\n \" or Drop implementation\",\n name,\n file,\n line,\n evidence_strength=[\"trait_impl\", \"sensitive_name_or_field\"],\n )\n )\n elif has_zeroize and not has_zeroize_on_drop and not has_drop:\n # Zeroize implemented but never auto-triggered\n findings.append(\n make_finding(\n \"MISSING_SOURCE_ZEROIZE\",\n \"high\",\n f\"Sensitive type '{name}' implements Zeroize but has no \"\n \"ZeroizeOnDrop or Drop to trigger it automatically\",\n name,\n file,\n line,\n evidence_strength=[\"trait_impl\", \"sensitive_name_or_field\"],\n )\n )\n\n # --- 3. Partial Drop: Drop impl present but not all secret fields zeroed ---\n if has_drop and kind == \"struct\":\n fields = struct_fields(item, index)\n secret_fields = [f for f in fields if SENSITIVE_FIELD_RE.search(f.get(\"name\") or \"\")]\n if secret_fields:\n # Find the Drop impl and check whether it zeroes all secret fields.\n drop_impls = _find_drop_impl_items(item, index)\n if drop_impls:\n secret_field_names = [f.get(\"name\") or \"\" for f in secret_fields]\n zeroed_names, evidence_strength = _zeroed_field_names_in_drop(\n drop_impls[0], index, secret_field_names\n )\n unzeroed = [\n f.get(\"name\") for f in secret_fields if f.get(\"name\") not in zeroed_names\n ]\n if unzeroed:\n severity = \"high\" if \"drop_body_source\" in evidence_strength else \"medium\"\n findings.append(\n make_finding(\n \"PARTIAL_WIPE\",\n severity,\n f\"Drop impl for '{name}' does not zero all secret fields: \"\n f\"missing {unzeroed}\",\n name,\n file,\n line,\n evidence_strength=evidence_strength + [\"trait_impl\"],\n )\n )\n elif \"drop_body_source\" not in evidence_strength:\n findings.append(\n make_finding(\n \"PARTIAL_WIPE\",\n \"medium\",\n f\"Drop impl for '{name}' found, but field-level \"\n \"zeroization could not be confirmed from \"\n \"function body; review manually\",\n name,\n file,\n line,\n evidence_strength=evidence_strength + [\"trait_impl\"],\n )\n )\n\n # --- 4. ZeroizeOnDrop with heap (Vec/Box) fields ---\n if has_zeroize_on_drop and kind == \"struct\":\n fields = struct_fields(item, index)\n heap_fields = _heap_fields(fields, index, source_file=file)\n alias_review = \"__alias_review__\" in heap_fields\n real_heap_fields = [f for f in heap_fields if f != \"__alias_review__\"]\n if real_heap_fields:\n findings.append(\n make_finding(\n \"PARTIAL_WIPE\",\n \"medium\",\n f\"ZeroizeOnDrop on '{name}' which has heap fields {real_heap_fields} — \"\n \"capacity bytes beyond len may not be zeroed\",\n name,\n file,\n line,\n evidence_strength=[\"resolved_path\", \"generic_traversal\", \"trait_impl\"],\n )\n )\n elif alias_review:\n findings.append(\n make_finding(\n \"PARTIAL_WIPE\",\n \"medium\",\n f\"ZeroizeOnDrop on '{name}' — source file contains type aliases that may \"\n \"wrap heap types (Vec/Box/String); verify all heap fields are covered\",\n name,\n file,\n line,\n evidence_strength=[\"alias_heuristic\", \"source_scan\", \"trait_impl\"],\n )\n )\n\n # --- 4b. ManuallyDrop\u003cT> field on sensitive struct ---\n if kind == \"struct\":\n fields = struct_fields(item, index)\n md_fields = _manually_drop_fields(fields, index)\n if md_fields:\n findings.append(\n make_finding(\n \"MISSING_SOURCE_ZEROIZE\",\n \"critical\",\n f\"Sensitive struct '{name}' has ManuallyDrop\u003cT> field(s) {md_fields} — \"\n \"Drop does not run automatically on ManuallyDrop fields; \"\n \"secret is not zeroed unless ManuallyDrop::drop() is called explicitly\",\n name,\n file,\n line,\n evidence_strength=[\"resolved_path\", \"trait_impl\"],\n )\n )\n\n # --- 5. Clone on zeroizing type ---\n if CLONE_DERIVES & derives and (has_zeroize or has_zeroize_on_drop or has_drop):\n findings.append(\n make_finding(\n \"SECRET_COPY\",\n \"medium\",\n f\"Clone on zeroizing type '{name}' — each clone is an independent allocation \"\n \"that must be independently zeroed\",\n name,\n file,\n line,\n evidence_strength=[\"attr_only\", \"trait_impl\"],\n )\n )\n\n # --- 6. From/Into returning non-zeroizing type ---\n from_into_escapes = _find_from_into_non_zeroizing(item, index)\n for escape, evidence_strength in from_into_escapes:\n findings.append(\n make_finding(\n \"SECRET_COPY\",\n \"medium\",\n f\"'{name}' has {escape} conversion returning a non-zeroizing type — \"\n \"bytes escape into caller's ownership in a non-zeroizing container\",\n name,\n file,\n line,\n evidence_strength=evidence_strength + [\"trait_impl\"],\n )\n )\n\n # --- 7. ptr::write_bytes without compiler_fence ---\n if _has_write_bytes_without_compiler_fence(file):\n findings.append(\n make_finding(\n \"OPTIMIZED_AWAY_ZEROIZE\",\n \"medium\",\n f\"'{name}' is defined in a file that uses ptr::write_bytes without \"\n \"compiler_fence — wipe may be optimized away by the compiler\",\n name,\n file,\n line,\n evidence_strength=[\"source_scan\", \"heuristic\"],\n )\n )\n\n # --- 8. cfg(feature) wrapping Drop/Zeroize ---\n if _has_cfg_feature_on_cleanup(item, index):\n findings.append(\n make_finding(\n \"NOT_ON_ALL_PATHS\",\n \"medium\",\n f\"#[cfg(feature=...)] wraps Drop or Zeroize impl for '{name}' — \"\n \"zeroing absent when feature flag is off\",\n name,\n file,\n line,\n evidence_strength=[\"attr_only\", \"trait_impl\"],\n )\n )\n\n # --- 9. Debug derive ---\n if DEBUG_DERIVES & derives:\n findings.append(\n make_finding(\n \"SECRET_COPY\",\n \"low\",\n f\"#[derive(Debug)] on sensitive type '{name}' — \"\n \"secrets may appear in formatted output / log entries\",\n name,\n file,\n line,\n evidence_strength=[\"attr_only\"],\n )\n )\n\n # --- 10. Serialize derive ---\n if SERIALIZE_DERIVES & derives:\n findings.append(\n make_finding(\n \"SECRET_COPY\",\n \"low\",\n f\"#[derive(Serialize)] on sensitive type '{name}' — \"\n \"serialization creates an uncontrolled copy of secret bytes\",\n name,\n file,\n line,\n evidence_strength=[\"attr_only\"],\n )\n )\n\n # --- 11. No zeroize crate dependency ---\n # Only emit when Cargo.toml was provided and successfully parsed but did\n # not list zeroize. has_zeroize_dep is None when the path was omitted or\n # the file could not be parsed, which must not trigger a false finding.\n if has_zeroize_dep is False:\n findings.append(\n make_finding(\n \"MISSING_SOURCE_ZEROIZE\",\n \"low\",\n \"No 'zeroize' crate in Cargo.toml dependencies — \"\n \"all manual zeroing lacks approved-API guarantee\",\n \"\u003ccrate>\",\n str(cargo_toml_path or \"Cargo.toml\"),\n 1,\n evidence_strength=[\"cargo_toml\"],\n )\n )\n\n return findings\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _check_zeroize_dep(cargo_toml_path: str | None) -> bool | None:\n \"\"\"Return True/False if Cargo.toml was parsed, None if path absent or unreadable.\"\"\"\n if not cargo_toml_path:\n return None\n try:\n content = Path(cargo_toml_path).read_text(encoding=\"utf-8\")\n manifest = tomllib.loads(content)\n except OSError:\n return None\n except tomllib.TOMLDecodeError as e:\n print(\n f\"semantic_audit.py: warning: cannot parse Cargo.toml {cargo_toml_path!r}: {e}\",\n file=sys.stderr,\n )\n return None\n return _manifest_has_zeroize_dep(manifest)\n\n\ndef _manifest_has_zeroize_dep(manifest: dict) -> bool:\n return any(_dep_table_has_zeroize(dep_table) for dep_table in _iter_dependency_tables(manifest))\n\n\ndef _iter_dependency_tables(manifest: dict) -> list[dict]:\n dep_tables: list[dict] = []\n\n dependencies = manifest.get(\"dependencies\")\n if isinstance(dependencies, dict):\n dep_tables.append(dependencies)\n\n workspace = manifest.get(\"workspace\")\n if isinstance(workspace, dict):\n workspace_deps = workspace.get(\"dependencies\")\n if isinstance(workspace_deps, dict):\n dep_tables.append(workspace_deps)\n\n target = manifest.get(\"target\")\n if isinstance(target, dict):\n for target_data in target.values():\n if not isinstance(target_data, dict):\n continue\n target_deps = target_data.get(\"dependencies\")\n if isinstance(target_deps, dict):\n dep_tables.append(target_deps)\n\n return dep_tables\n\n\ndef _dep_table_has_zeroize(dep_table: dict) -> bool:\n for dep_name, dep_spec in dep_table.items():\n if isinstance(dep_name, str) and dep_name.lower() == \"zeroize\":\n return True\n if isinstance(dep_spec, dict):\n package_name = dep_spec.get(\"package\")\n if isinstance(package_name, str) and package_name.lower() == \"zeroize\":\n return True\n return False\n\n\ndef _find_drop_impl_items(item: dict, index: dict) -> list[dict]:\n result = []\n for impl_id in item.get(\"impls\") or []:\n impl_item = index.get(str(impl_id)) or {}\n inner = impl_item.get(\"inner\") or {}\n impl_data = inner.get(\"impl\") or {}\n trait_ref = impl_data.get(\"trait\") or {}\n if _trait_name(trait_ref) == \"Drop\":\n result.append(impl_item)\n return result\n\n\ndef _zeroed_field_names_in_drop(\n drop_impl: dict, index: dict, secret_fields: list[str]\n) -> tuple[set[str], list[str]]:\n \"\"\"\n Extract zeroed fields from Drop evidence.\n\n Prefers parsing Drop::drop source span. Falls back to docs text when source\n body is unavailable.\n \"\"\"\n body = _extract_drop_body_from_impl(drop_impl, index)\n if body:\n return _zeroed_field_names_in_text(body, secret_fields), [\"drop_body_source\"]\n\n docs = drop_impl.get(\"docs\") or \"\"\n if docs:\n return _zeroed_field_names_in_text(docs, secret_fields), [\"docs_heuristic\"]\n\n return set(), [\"unavailable\"]\n\n\ndef _extract_drop_body_from_impl(drop_impl: dict, index: dict) -> str:\n inner = drop_impl.get(\"inner\") or {}\n impl_data = inner.get(\"impl\") or {}\n for method_id in impl_data.get(\"items\") or []:\n method_item = index.get(str(method_id)) or {}\n if (method_item.get(\"kind\") or \"\") != \"function\":\n continue\n if (method_item.get(\"name\") or \"\") != \"drop\":\n continue\n source = _read_item_span_source(method_item)\n if source:\n return source\n return \"\"\n\n\ndef _read_item_span_source(item: dict) -> str:\n span = item.get(\"span\") or {}\n filename = span.get(\"filename\")\n begin = span.get(\"begin\") or []\n end = span.get(\"end\") or []\n if not filename or not begin or not end:\n return \"\"\n try:\n lines = Path(filename).read_text(encoding=\"utf-8\", errors=\"replace\").splitlines()\n except OSError as e:\n print(\n f\"semantic_audit.py: warning: cannot read span source {filename!r}: {e}\",\n file=sys.stderr,\n )\n return \"\"\n\n start_line = max(int(begin[0]), 1)\n end_line = max(int(end[0]), start_line)\n if start_line > len(lines):\n return \"\"\n snippet = lines[start_line - 1 : min(end_line, len(lines))]\n return \"\\n\".join(snippet)\n\n\ndef _zeroed_field_names_in_text(text: str, field_names: list[str]) -> set[str]:\n zeroed: set[str] = set()\n for field_name in field_names:\n escaped = re.escape(field_name)\n patterns = [\n rf\"\\bself\\.{escaped}\\.zeroize\\s*\\(\",\n rf\"\\bzeroize\\s*\\(\\s*&mut\\s+self\\.{escaped}\\s*\\)\",\n rf\"\\bself\\.{escaped}\\s*=\\s*(?:0+|Default::default\\(\\)|\\[[^]]+\\])\",\n rf\"\\bself\\.{escaped}\\.fill\\s*\\(\\s*0\\s*\\)\",\n ]\n if any(re.search(pattern, text) for pattern in patterns):\n zeroed.add(field_name)\n return zeroed\n\n\n# Matches type alias definitions like: type SecretBuffer = Vec\u003cu8>;\n_TYPE_ALIAS_RE = re.compile(\n r\"^\\s*(?:pub\\s+)?type\\s+\\w+\\s*=\\s*(?:Vec|Box|String|HashMap|BTreeMap)\\b\"\n)\n\n\ndef _heap_fields(fields: list[dict], index: dict, source_file: str | None = None) -> list[str]:\n heap: list[str] = []\n for field in fields:\n fname = field.get(\"name\") or \"\"\n inner = field.get(\"inner\") or {}\n struct_field = inner.get(\"struct_field\") or {}\n ty = struct_field.get(\"type\") or {}\n if _type_contains_heap(ty, index):\n heap.append(fname)\n # If no heap fields found via rustdoc, scan the source file for type aliases\n # that may wrap heap types (e.g. `type SecretBuffer = Vec\u003cu8>`). Emit a\n # needs_review note by appending a sentinel value so callers can detect this.\n if not heap and source_file:\n try:\n src = Path(source_file).read_text(encoding=\"utf-8\", errors=\"replace\")\n if _TYPE_ALIAS_RE.search(src):\n heap.append(\"__alias_review__\")\n except OSError as e:\n print(\n f\"semantic_audit.py: warning: cannot read source\"\n f\" for alias scan {source_file!r}: {e}\",\n file=sys.stderr,\n )\n return heap\n\n\ndef _manually_drop_fields(fields: list[dict], index: dict) -> list[str]:\n \"\"\"Return field names whose type is or contains ManuallyDrop\u003cT>.\"\"\"\n result: list[str] = []\n for field in fields:\n fname = field.get(\"name\") or \"\"\n inner = field.get(\"inner\") or {}\n struct_field = inner.get(\"struct_field\") or {}\n ty = struct_field.get(\"type\") or {}\n names = _type_named_paths(ty, index, set())\n if MANUALLY_DROP_NAMES & names:\n result.append(fname)\n return result\n\n\ndef _find_from_into_non_zeroizing(item: dict, index: dict) -> list[tuple[str, list[str]]]:\n escapes: list[tuple[str, list[str]]] = []\n for impl_id in item.get(\"impls\") or []:\n impl_item = index.get(str(impl_id)) or {}\n inner = impl_item.get(\"inner\") or {}\n impl_data = inner.get(\"impl\") or {}\n trait_ref = impl_data.get(\"trait\") or {}\n tname = _trait_name(trait_ref)\n if tname not in (\"From\", \"Into\"):\n continue\n for target_type in _iter_trait_type_args(trait_ref):\n if _type_is_zeroizing(target_type, index):\n continue\n target_desc = _type_description(target_type, index)\n evidence = (\n [\"resolved_path\", \"generic_traversal\"]\n if _type_has_resolved_path(target_type)\n else [\"alias_heuristic\"]\n )\n escapes.append((f\"{tname}\u003c{target_desc}>\", evidence))\n return escapes\n\n\ndef _iter_trait_type_args(trait_ref: dict) -> list[dict]:\n args = trait_ref.get(\"args\") or {}\n angle = args.get(\"angle_bracketed\") or {}\n out: list[dict] = []\n for arg in angle.get(\"args\") or []:\n ty = arg.get(\"type\")\n if isinstance(ty, dict):\n out.append(ty)\n return out\n\n\ndef _type_contains_heap(ty: dict[str, Any], index: dict, seen: set[str] | None = None) -> bool:\n seen = seen or set()\n return any(name in HEAP_TYPE_NAMES for name in _type_named_paths(ty, index, seen))\n\n\ndef _type_is_zeroizing(ty: dict[str, Any], index: dict, seen: set[str] | None = None) -> bool:\n seen = seen or set()\n names = _type_named_paths(ty, index, seen)\n if any(name in ZEROIZING_WRAPPER_NAMES for name in names):\n return True\n return any(ZEROIZING_NAME_HINT_RE.search(name) for name in names)\n\n\ndef _type_has_resolved_path(ty: dict[str, Any]) -> bool:\n if not isinstance(ty, dict):\n return False\n if \"resolved_path\" in ty:\n return True\n return any(_type_has_resolved_path(nested) for nested in _iter_nested_types(ty))\n\n\ndef _type_description(ty: dict[str, Any], index: dict) -> str:\n names = sorted(_type_named_paths(ty, index, set()))\n if names:\n return \"::\".join(names[:2]) if len(names) > 1 else names[0]\n return \"unknown\"\n\n\ndef _type_named_paths(ty: dict[str, Any], index: dict, seen_alias_ids: set[str]) -> set[str]:\n names: set[str] = set()\n if not isinstance(ty, dict):\n return names\n\n resolved = ty.get(\"resolved_path\")\n if isinstance(resolved, dict):\n raw_name = resolved.get(\"name\")\n if isinstance(raw_name, str) and raw_name:\n names.add(raw_name.split(\"::\")[-1])\n\n alias_id = resolved.get(\"id\")\n alias_item = index.get(str(alias_id)) if alias_id is not None else None\n alias_id_str = str(alias_id) if alias_id is not None else \"\"\n if (\n alias_id_str\n and alias_id_str not in seen_alias_ids\n and isinstance(alias_item, dict)\n and (alias_item.get(\"kind\") or \"\") == \"typedef\"\n ):\n seen_alias_ids.add(alias_id_str)\n alias_type = ((alias_item.get(\"inner\") or {}).get(\"type_alias\") or {}).get(\"type\") or {}\n names |= _type_named_paths(alias_type, index, seen_alias_ids)\n\n args = resolved.get(\"args\") or {}\n names |= _type_args_named_paths(args, index, seen_alias_ids)\n\n for nested in _iter_nested_types(ty):\n names |= _type_named_paths(nested, index, seen_alias_ids)\n return names\n\n\ndef _type_args_named_paths(args: dict[str, Any], index: dict, seen_alias_ids: set[str]) -> set[str]:\n names: set[str] = set()\n angle = args.get(\"angle_bracketed\") if isinstance(args, dict) else None\n if not isinstance(angle, dict):\n return names\n for arg in angle.get(\"args\") or []:\n if isinstance(arg, dict):\n ty = arg.get(\"type\")\n if isinstance(ty, dict):\n names |= _type_named_paths(ty, index, seen_alias_ids)\n return names\n\n\ndef _iter_nested_types(ty: dict[str, Any]) -> list[dict[str, Any]]:\n nested: list[dict[str, Any]] = []\n\n borrowed = ty.get(\"borrowed_ref\")\n if isinstance(borrowed, dict):\n inner_ty = borrowed.get(\"type\")\n if isinstance(inner_ty, dict):\n nested.append(inner_ty)\n\n raw_ptr = ty.get(\"raw_pointer\")\n if isinstance(raw_ptr, dict):\n inner_ty = raw_ptr.get(\"type\")\n if isinstance(inner_ty, dict):\n nested.append(inner_ty)\n\n array_ty = ty.get(\"array\")\n if isinstance(array_ty, dict):\n inner_ty = array_ty.get(\"type\")\n if isinstance(inner_ty, dict):\n nested.append(inner_ty)\n\n slice_ty = ty.get(\"slice\")\n if isinstance(slice_ty, dict):\n nested.append(slice_ty)\n\n tuple_types = ty.get(\"tuple\")\n if isinstance(tuple_types, list):\n for inner_ty in tuple_types:\n if isinstance(inner_ty, dict):\n nested.append(inner_ty)\n\n qualified = ty.get(\"qualified_path\")\n if isinstance(qualified, dict):\n qself = qualified.get(\"self_type\")\n if isinstance(qself, dict):\n nested.append(qself)\n qtrait = qualified.get(\"trait\")\n if isinstance(qtrait, dict):\n nested.append(qtrait)\n\n return nested\n\n\n_COMPILER_FENCE_RE = re.compile(\n r\"\\b(?:core::sync::atomic::|std::sync::atomic::)?compiler_fence\\s*\\(\"\n)\n\n\ndef _has_write_bytes_without_compiler_fence(source_file: str | None) -> bool:\n if not source_file:\n return False\n try:\n src = Path(source_file).read_text(encoding=\"utf-8\", errors=\"replace\")\n except OSError:\n return False\n return \"write_bytes\" in src and not _COMPILER_FENCE_RE.search(src)\n\n\ndef _has_cfg_feature_on_cleanup(item: dict, index: dict) -> bool:\n for impl_id in item.get(\"impls\") or []:\n impl_item = index.get(str(impl_id)) or {}\n inner = impl_item.get(\"inner\") or {}\n impl_data = inner.get(\"impl\") or {}\n trait_ref = impl_data.get(\"trait\") or {}\n tname = _trait_name(trait_ref)\n if tname not in (\"Drop\", \"Zeroize\", \"ZeroizeOnDrop\"):\n continue\n for attr in impl_item.get(\"attrs\") or []:\n if \"cfg\" in attr and \"feature\" in attr:\n return True\n return False\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\n\ndef main() -> int:\n parser = argparse.ArgumentParser(\n description=\"Rust trait-aware zeroization auditor (rustdoc JSON input)\"\n )\n parser.add_argument(\"--rustdoc\", required=True, help=\"Path to rustdoc JSON file\")\n parser.add_argument(\"--cargo-toml\", help=\"Path to Cargo.toml (for dependency checks)\")\n parser.add_argument(\"--out\", required=True, help=\"Output findings JSON path\")\n args = parser.parse_args()\n\n rustdoc_path = Path(args.rustdoc)\n if not rustdoc_path.exists():\n print(f\"semantic_audit.py: rustdoc JSON not found: {rustdoc_path}\", file=sys.stderr)\n return 1\n\n try:\n rustdoc = json.loads(rustdoc_path.read_text(encoding=\"utf-8\"))\n except (json.JSONDecodeError, OSError) as e:\n print(f\"semantic_audit.py: failed to parse rustdoc JSON: {e}\", file=sys.stderr)\n return 1\n\n findings = analyze(rustdoc, args.cargo_toml)\n\n out_path = Path(args.out)\n out_path.parent.mkdir(parents=True, exist_ok=True)\n out_path.write_text(json.dumps(findings, indent=2), encoding=\"utf-8\")\n\n print(f\"semantic_audit.py: {len(findings)} finding(s) written to {out_path}\")\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":33243,"content_sha256":"b84e3e885e07f88dce17da03d067c1bf7a7ade0305209bdc2e9be28ecad757d9"},{"filename":"tools/track_dataflow.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# Track data-flow of sensitive variables to detect untracked copies.\n#\n# Usage:\n# track_dataflow.sh --src path/to/file.c --config config.yaml --out /tmp/dataflow.json\n#\n# Detects:\n# - memcpy/memmove of sensitive buffers\n# - Struct assignments (potential copies)\n# - Function arguments passed by value\n# - Return by value (secrets in return values)\n\nusage() {\n echo \"Usage: $0 --src \u003cfile> --out \u003canalysis.json> [--config \u003cconfig.yaml>]\" >&2\n}\n\njson_escape() {\n local s=\"$1\"\n s=\"${s//\\\\/\\\\\\\\}\"\n s=\"${s//\\\"/\\\\\\\"}\"\n s=\"${s//

zeroize-audit — Claude Skill When to Use - Auditing cryptographic implementations (keys, seeds, nonces, secrets) - Reviewing authentication systems (passwords, tokens, session data) - Analyzing code that handles PII or sensitive credentials - Verifying secure cleanup in security-critical codebases - Investigating memory safety of sensitive data handling When NOT to Use - General code review without security focus - Performance optimization (unless related to secure wiping) - Refactoring tasks not related to sensitive data - Code without identifiable secrets or sensitive values --- Purpose Det…

\\n'/\\\\n}\"\n s=\"${s//

zeroize-audit — Claude Skill When to Use - Auditing cryptographic implementations (keys, seeds, nonces, secrets) - Reviewing authentication systems (passwords, tokens, session data) - Analyzing code that handles PII or sensitive credentials - Verifying secure cleanup in security-critical codebases - Investigating memory safety of sensitive data handling When NOT to Use - General code review without security focus - Performance optimization (unless related to secure wiping) - Refactoring tasks not related to sensitive data - Code without identifiable secrets or sensitive values --- Purpose Det…

\\t'/\\\\t}\"\n printf '%s' \"$s\"\n}\n\nSRC=\"\"\nCONFIG=\"\"\nOUT=\"\"\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --src)\n SRC=\"$2\"\n shift 2\n ;;\n --config)\n CONFIG=\"$2\"\n shift 2\n ;;\n --out)\n OUT=\"$2\"\n shift 2\n ;;\n *)\n echo \"Unknown arg: $1\" >&2\n usage\n exit 2\n ;;\n esac\ndone\n\nif [[ -z \"$SRC\" || -z \"$OUT\" ]]; then\n usage\n exit 2\nfi\n\nif [[ ! -f \"$SRC\" ]]; then\n echo \"Source file not found: $SRC\" >&2\n exit 2\nfi\n\n# Load sensitive name patterns from config (if provided)\nSENSITIVE_PATTERN=\"(secret|key|seed|priv|private|sk|shared_secret|nonce|token|pwd|pass)\"\nif [[ -n \"$CONFIG\" ]] && [[ -f \"$CONFIG\" ]]; then\n # Extract patterns from YAML (POSIX-compatible, no grep -P)\n PATTERNS=$(grep -A 20 \"^sensitive_name_regex:\" \"$CONFIG\" | sed -n 's/.*\"\\([^\"]*\\)\".*/\\1/p' | head -1 || echo \"\")\n if [[ -n \"$PATTERNS\" ]]; then\n SENSITIVE_PATTERN=\"$PATTERNS\"\n else\n echo \"WARNING: config file provided but no patterns extracted from $CONFIG\" >&2\n fi\nfi\n\n# Arrays to collect findings\nMEMCPY_COPIES=()\nSTRUCT_ASSIGNS=()\nFUNC_ARGS=()\nRETURN_VALUES=()\nRETURN_RE='return[[:space:]]+([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*;'\nCALL_RE='([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*\\(([^)]*)\\)'\n\n# Parse source code\nLINE_NUM=0\nIN_FUNCTION=\"\"\n\nwhile IFS= read -r line; do\n ((LINE_NUM++))\n\n # Skip comments (simple heuristic)\n [[ \"$line\" =~ ^[[:space:]]*// ]] && continue\n [[ \"$line\" =~ ^[[:space:]]*\\* ]] && continue\n\n # Track function boundaries\n if [[ \"$line\" =~ ^[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]+([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*\\( ]]; then\n IN_FUNCTION=\"${BASH_REMATCH[1]}\"\n fi\n\n # Detect memcpy/memmove of sensitive data\n if [[ \"$line\" =~ (memcpy|memmove)[[:space:]]*\\([^,]*,[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*) ]]; then\n FUNC=\"${BASH_REMATCH[1]}\"\n SRC_VAR=\"${BASH_REMATCH[2]}\"\n if [[ \"$SRC_VAR\" =~ $SENSITIVE_PATTERN ]]; then\n MEMCPY_COPIES+=(\"{\\\"line\\\": $LINE_NUM, \\\"function\\\": \\\"$FUNC\\\", \\\"variable\\\": \\\"$SRC_VAR\\\", \\\"context\\\": \\\"$(json_escape \"$line\")\\\"}\")\n fi\n fi\n\n # Detect struct assignments (potential copies)\n if [[ \"$line\" =~ ([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*\\*([a-zA-Z_][a-zA-Z0-9_]*) ]]; then\n DEST=\"${BASH_REMATCH[1]}\"\n MATCH_SRC=\"${BASH_REMATCH[2]}\"\n if [[ \"$MATCH_SRC\" =~ $SENSITIVE_PATTERN ]] || [[ \"$DEST\" =~ $SENSITIVE_PATTERN ]]; then\n STRUCT_ASSIGNS+=(\"{\\\"line\\\": $LINE_NUM, \\\"dest\\\": \\\"$DEST\\\", \\\"source\\\": \\\"$MATCH_SRC\\\", \\\"context\\\": \\\"$(json_escape \"$line\")\\\"}\")\n fi\n fi\n\n # Detect return by value\n if [[ \"$line\" =~ $RETURN_RE ]]; then\n RET_VAR=\"${BASH_REMATCH[1]}\"\n if [[ \"$RET_VAR\" =~ $SENSITIVE_PATTERN ]]; then\n RETURN_VALUES+=(\"{\\\"line\\\": $LINE_NUM, \\\"function\\\": \\\"$IN_FUNCTION\\\", \\\"variable\\\": \\\"$RET_VAR\\\", \\\"context\\\": \\\"$(json_escape \"$line\")\\\"}\")\n fi\n fi\n\n # Detect function calls with sensitive arguments (simple heuristic)\n if [[ \"$line\" =~ $CALL_RE ]]; then\n CALLED_FUNC=\"${BASH_REMATCH[1]}\"\n ARGS=\"${BASH_REMATCH[2]}\"\n # Check if any argument matches sensitive pattern\n if [[ \"$ARGS\" =~ $SENSITIVE_PATTERN ]]; then\n # Extract variable names from arguments\n for arg in ${ARGS//,/ }; do\n arg=\"${arg#\"${arg%%[! ]*}\"}\" # trim leading spaces\n arg=\"${arg%\"${arg##*[! ]}\"}\" # trim trailing spaces\n if [[ \"$arg\" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] && [[ \"$arg\" =~ $SENSITIVE_PATTERN ]]; then\n FUNC_ARGS+=(\"{\\\"line\\\": $LINE_NUM, \\\"called_function\\\": \\\"$CALLED_FUNC\\\", \\\"argument\\\": \\\"$arg\\\", \\\"context\\\": \\\"$(json_escape \"$line\")\\\"}\")\n fi\n done\n fi\n fi\n\ndone \u003c\"$SRC\"\n\n# Generate JSON report\nmkdir -p \"$(dirname \"$OUT\")\"\n\ncat >\"$OUT\" \u003c\u003cEOF\n{\n \"source_file\": \"$SRC\",\n \"sensitive_pattern\": \"$SENSITIVE_PATTERN\",\n \"findings\": {\n \"memcpy_copies\": [\n $(\n IFS=,\n echo \"${MEMCPY_COPIES[*]}\"\n)\n ],\n \"struct_assignments\": [\n $(\n IFS=,\n echo \"${STRUCT_ASSIGNS[*]}\"\n)\n ],\n \"function_arguments\": [\n $(\n IFS=,\n echo \"${FUNC_ARGS[*]}\"\n)\n ],\n \"return_values\": [\n $(\n IFS=,\n echo \"${RETURN_VALUES[*]}\"\n)\n ]\n },\n \"summary\": {\n \"total_copies\": $((${#MEMCPY_COPIES[@]} + ${#STRUCT_ASSIGNS[@]} + ${#FUNC_ARGS[@]} + ${#RETURN_VALUES[@]})),\n \"memcpy_count\": ${#MEMCPY_COPIES[@]},\n \"struct_assign_count\": ${#STRUCT_ASSIGNS[@]},\n \"func_arg_count\": ${#FUNC_ARGS[@]},\n \"return_value_count\": ${#RETURN_VALUES[@]}\n }\n}\nEOF\n\n# Validate JSON output\nif command -v jq &>/dev/null; then\n if ! jq empty \"$OUT\" 2>/dev/null; then\n echo \"ERROR: generated JSON is malformed: $OUT\" >&2\n exit 1\n fi\nfi\n\necho \"OK: data-flow analysis written to $OUT\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5209,"content_sha256":"c60641893a664589d43c837b2ea4c7999ca51dcf61896455e97df4af0a9c12d0"},{"filename":"tools/validate_rust_toolchain.sh","content":"#!/usr/bin/env bash\n# validate_rust_toolchain.sh — Preflight check for Rust zeroize-audit prerequisites.\n#\n# Validates that all tools required by the Rust analysis pipeline are available\n# and functional. Outputs a JSON status report.\n#\n# Exit codes:\n# 0 all required tools available (warnings may still be present)\n# 1 at least one required tool is missing\n# 2 argument error\n\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\nusage() {\n cat \u003c\u003c'EOF'\nUsage:\n validate_rust_toolchain.sh [options]\n\nOptions:\n --manifest \u003cCargo.toml> Check that the manifest exists and the crate builds\n --json Output machine-readable JSON (default: human-readable)\n --help Show this help text\n\nChecks (required):\n - cargo on PATH\n - cargo +nightly available\n - uv on PATH (for Python analysis scripts)\n\nChecks (optional, warning only):\n - rustfilt on PATH (for symbol demangling)\n - cargo-expand on PATH (for macro expansion debugging)\n\nIf --manifest is provided, additionally:\n - Manifest file exists\n - cargo check passes for the crate\nEOF\n}\n\ndie_arg() {\n echo \"validate_rust_toolchain.sh: $*\" >&2\n exit 2\n}\n\nMANIFEST=\"\"\nJSON_OUTPUT=false\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --manifest)\n [[ -n \"${2-}\" ]] || die_arg \"missing value for --manifest\"\n MANIFEST=\"$2\"\n shift 2\n ;;\n --json)\n JSON_OUTPUT=true\n shift\n ;;\n --help | -h)\n usage\n exit 0\n ;;\n *)\n die_arg \"unknown argument: $1\"\n ;;\n esac\ndone\n\n# ---------------------------------------------------------------------------\n# Tool checks\n# ---------------------------------------------------------------------------\n\ndeclare -A TOOL_STATUS\ndeclare -A TOOL_VERSION\nERRORS=()\nWARNINGS=()\n\ncheck_tool() {\n local name=\"$1\"\n local required=\"$2\"\n local cmd=\"${3:-$name}\"\n\n if command -v \"$cmd\" &>/dev/null; then\n TOOL_STATUS[\"$name\"]=\"present\"\n local ver\n # Use a separate variable so we can distinguish a version-check failure\n # (e.g. shared-library missing) from the tool simply not being on PATH.\n if ver=$(\"$cmd\" --version 2>/dev/null | head -1); then\n TOOL_VERSION[\"$name\"]=\"$ver\"\n else\n TOOL_VERSION[\"$name\"]=\"(version check failed)\"\n WARNINGS+=(\"$name is present but '--version' failed — tool may be broken\")\n fi\n else\n if [[ \"$required\" == \"true\" ]]; then\n TOOL_STATUS[\"$name\"]=\"missing\"\n ERRORS+=(\"$name is required but not found on PATH\")\n else\n TOOL_STATUS[\"$name\"]=\"missing\"\n WARNINGS+=(\"$name is not found on PATH (optional: ${4:-enhanced analysis})\")\n fi\n fi\n}\n\ncheck_tool \"cargo\" \"true\"\ncheck_tool \"uv\" \"true\"\ncheck_tool \"rustfilt\" \"false\" \"rustfilt\" \"Rust symbol demangling in assembly analysis\"\ncheck_tool \"cargo-expand\" \"false\" \"cargo-expand\" \"macro expansion debugging\"\n\n# Check cargo +nightly\nNIGHTLY_STATUS=\"unavailable\"\nNIGHTLY_VERSION=\"unknown\"\nif [[ \"${TOOL_STATUS[cargo]}\" == \"present\" ]]; then\n if cargo +nightly --version &>/dev/null 2>&1; then\n NIGHTLY_STATUS=\"available\"\n NIGHTLY_VERSION=$(cargo +nightly --version 2>/dev/null | head -1 || echo \"unknown\")\n else\n NIGHTLY_STATUS=\"unavailable\"\n ERRORS+=(\"cargo +nightly is required but the nightly toolchain is not installed (run: rustup toolchain install nightly)\")\n fi\nfi\n\n# Check that emit/analysis scripts exist\ndeclare -A SCRIPT_STATUS\nREQUIRED_SCRIPTS=(\n \"emit_rust_mir.sh\"\n \"emit_rust_ir.sh\"\n \"emit_rust_asm.sh\"\n)\nOPTIONAL_SCRIPTS=(\n \"diff_rust_mir.sh\"\n \"scripts/check_mir_patterns.py\"\n \"scripts/check_llvm_patterns.py\"\n \"scripts/check_rust_asm.py\"\n \"scripts/semantic_audit.py\"\n \"scripts/find_dangerous_apis.py\"\n)\n\nfor script in \"${REQUIRED_SCRIPTS[@]}\"; do\n if [[ -f \"$SCRIPT_DIR/$script\" ]]; then\n SCRIPT_STATUS[\"$script\"]=\"present\"\n else\n SCRIPT_STATUS[\"$script\"]=\"missing\"\n ERRORS+=(\"required script $script not found at $SCRIPT_DIR/$script\")\n fi\ndone\n\nfor script in \"${OPTIONAL_SCRIPTS[@]}\"; do\n if [[ -f \"$SCRIPT_DIR/$script\" ]]; then\n SCRIPT_STATUS[\"$script\"]=\"present\"\n else\n SCRIPT_STATUS[\"$script\"]=\"missing\"\n WARNINGS+=(\"optional script $script not found at $SCRIPT_DIR/$script\")\n fi\ndone\n\n# Check manifest and crate build (if requested)\nMANIFEST_STATUS=\"not_checked\"\nBUILD_STATUS=\"not_checked\"\nif [[ -n \"$MANIFEST\" ]]; then\n if [[ -f \"$MANIFEST\" ]]; then\n MANIFEST_STATUS=\"present\"\n if [[ \"$NIGHTLY_STATUS\" == \"available\" ]]; then\n cargo_err=$(mktemp) || {\n ERRORS+=(\"mktemp failed — cannot capture cargo output\")\n cargo_err=\"/dev/null\"\n }\n if cargo +nightly check --manifest-path \"$MANIFEST\" 2>\"$cargo_err\"; then\n BUILD_STATUS=\"pass\"\n else\n BUILD_STATUS=\"fail\"\n # Include up to 20 lines of cargo output so callers can diagnose\n # the failure without re-running manually.\n cargo_snippet=$(head -20 \"$cargo_err\" 2>/dev/null | tr '\\n' ' ')\n ERRORS+=(\"cargo check failed for $MANIFEST: ${cargo_snippet:-see stderr}\")\n fi\n rm -f \"$cargo_err\"\n else\n BUILD_STATUS=\"skipped\"\n WARNINGS+=(\"cargo check skipped (nightly not available)\")\n fi\n else\n MANIFEST_STATUS=\"missing\"\n ERRORS+=(\"manifest not found: $MANIFEST\")\n fi\nfi\n\n# ---------------------------------------------------------------------------\n# Output\n# ---------------------------------------------------------------------------\n\nOVERALL_STATUS=\"ready\"\n[[ ${#ERRORS[@]} -gt 0 ]] && OVERALL_STATUS=\"blocked\"\n\nif [[ \"$JSON_OUTPUT\" == true ]]; then\n # Build tool statuses as JSON\n TOOLS_JSON=\"{\"\n first=true\n for name in cargo uv rustfilt cargo-expand; do\n [[ \"$first\" == true ]] && first=false || TOOLS_JSON+=\",\"\n TOOLS_JSON+=\"\\\"$name\\\":{\\\"status\\\":\\\"${TOOL_STATUS[$name]:-unknown}\\\",\\\"version\\\":\\\"${TOOL_VERSION[$name]:-unknown}\\\"}\"\n done\n TOOLS_JSON+=\"}\"\n\n # Build script statuses as JSON\n SCRIPTS_JSON=\"{\"\n first=true\n for script in \"${REQUIRED_SCRIPTS[@]}\" \"${OPTIONAL_SCRIPTS[@]}\"; do\n [[ \"$first\" == true ]] && first=false || SCRIPTS_JSON+=\",\"\n SCRIPTS_JSON+=\"\\\"$script\\\":\\\"${SCRIPT_STATUS[$script]:-unknown}\\\"\"\n done\n SCRIPTS_JSON+=\"}\"\n\n # Build errors/warnings arrays using Python for correct JSON escaping.\n # The sed-based approach only escaped double quotes but not backslashes,\n # newlines, or control characters — all of which can appear in cargo output.\n _json_str_array() {\n # Read lines from stdin, emit a JSON array of properly escaped strings.\n python3 -c '\nimport json, sys\nitems = [l.rstrip(\"\\n\") for l in sys.stdin]\nprint(json.dumps(items))\n'\n }\n ERRORS_JSON=$(printf '%s\\n' \"${ERRORS[@]+\"${ERRORS[@]}\"}\" | _json_str_array)\n WARNINGS_JSON=$(printf '%s\\n' \"${WARNINGS[@]+\"${WARNINGS[@]}\"}\" | _json_str_array)\n\n cat \u003c\u003cEOF\n{\n \"status\": \"$OVERALL_STATUS\",\n \"tools\": $TOOLS_JSON,\n \"nightly\": {\"status\": \"$NIGHTLY_STATUS\", \"version\": \"$NIGHTLY_VERSION\"},\n \"scripts\": $SCRIPTS_JSON,\n \"manifest\": {\"status\": \"$MANIFEST_STATUS\", \"build\": \"$BUILD_STATUS\"},\n \"errors\": $ERRORS_JSON,\n \"warnings\": $WARNINGS_JSON\n}\nEOF\nelse\n echo \"=== Rust Toolchain Validation ===\"\n echo \"\"\n echo \"Tools:\"\n for name in cargo uv rustfilt cargo-expand; do\n status=\"${TOOL_STATUS[$name]:-unknown}\"\n version=\"${TOOL_VERSION[$name]:-}\"\n if [[ \"$status\" == \"present\" ]]; then\n echo \" [OK] $name ($version)\"\n else\n echo \" [MISS] $name\"\n fi\n done\n echo \"\"\n echo \"Nightly: $NIGHTLY_STATUS ($NIGHTLY_VERSION)\"\n echo \"\"\n echo \"Scripts:\"\n for script in \"${REQUIRED_SCRIPTS[@]}\"; do\n status=\"${SCRIPT_STATUS[$script]:-unknown}\"\n if [[ \"$status\" == \"present\" ]]; then\n echo \" [OK] $script\"\n else\n echo \" [MISS] $script (required)\"\n fi\n done\n for script in \"${OPTIONAL_SCRIPTS[@]}\"; do\n status=\"${SCRIPT_STATUS[$script]:-unknown}\"\n if [[ \"$status\" == \"present\" ]]; then\n echo \" [OK] $script\"\n else\n echo \" [MISS] $script (optional)\"\n fi\n done\n\n if [[ -n \"$MANIFEST\" ]]; then\n echo \"\"\n echo \"Manifest: $MANIFEST ($MANIFEST_STATUS)\"\n echo \"Build: $BUILD_STATUS\"\n fi\n\n if [[ ${#ERRORS[@]} -gt 0 ]]; then\n echo \"\"\n echo \"ERRORS:\"\n for err in \"${ERRORS[@]}\"; do\n echo \" - $err\"\n done\n fi\n if [[ ${#WARNINGS[@]} -gt 0 ]]; then\n echo \"\"\n echo \"WARNINGS:\"\n for warn in \"${WARNINGS[@]}\"; do\n echo \" - $warn\"\n done\n fi\n echo \"\"\n echo \"Overall: $OVERALL_STATUS\"\nfi\n\n[[ \"$OVERALL_STATUS\" == \"ready\" ]]\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":8434,"content_sha256":"4f294646864874bb93ef0aaceecfcaa3f81743278ffe9c48540f53a70004f49f"},{"filename":"workflows/phase-0-preflight.md","content":"# Phase 0 — Preflight, Configuration, and Work Directory\n\n## Preconditions\n\nNone — this is the first phase.\n\n## Instructions\n\nSpawn agent `0-preflight` via `Task` with:\n\n| Parameter | Value |\n|---|---|\n| `path` | `{{path}}` |\n| `compile_db` | `{{compile_db}}` |\n| `cargo_manifest` | `{{cargo_manifest}}` |\n| `config` | `{{config}}` |\n| `languages` | `{{languages}}` |\n| `max_tus` | `{{max_tus}}` |\n| `mcp_mode` | `{{mcp_mode}}` |\n| `mcp_timeout_ms` | `{{mcp_timeout_ms}}` |\n| `mcp_required_for_advanced` | `{{mcp_required_for_advanced}}` |\n| `enable_asm` | `{{enable_asm}}` |\n| `enable_semantic_ir` | `{{enable_semantic_ir}}` |\n| `enable_cfg` | `{{enable_cfg}}` |\n| `enable_runtime_tests` | `{{enable_runtime_tests}}` |\n| `opt_levels` | `{{opt_levels}}` |\n| `poc_categories` | `{{poc_categories}}` |\n| `poc_output_dir` | `{{poc_output_dir}}` |\n| `baseDir` | `{baseDir}` |\n\nThe agent creates the work directory, runs all preflight checks, merges configuration, enumerates TUs, and writes `orchestrator-state.json`.\n\n### What the `0-preflight` agent must do\n\n**Step 1 — Determine language mode** from inputs:\n- `compile_db` set and `cargo_manifest` not set → `language_mode=c`\n- `cargo_manifest` set and `compile_db` not set → `language_mode=rust`\n- Both set → `language_mode=mixed`\n- Neither set → **stop the run**: at least one of `compile_db` or `cargo_manifest` is required.\n\n**Step 2 — C/C++ preflight** (skip if `language_mode=rust`):\n\n1. Verify `compile_db` file exists at the given path.\n2. Verify at least one entry in the compile DB resolves to an existing source file and working directory.\n3. Attempt a trial compilation of one representative TU using its captured flags to confirm the codebase is buildable.\n4. Verify `{baseDir}/tools/extract_compile_flags.py` exists and is executable.\n5. Verify `{baseDir}/tools/emit_ir.sh` exists and is executable.\n6. If `enable_asm=true`: verify `{baseDir}/tools/emit_asm.sh` exists; if missing, set `enable_asm=false` and emit a warning.\n7. If `mcp_mode != off`: run `{baseDir}/tools/mcp/check_mcp.sh` to probe MCP availability.\n - If `mcp_mode=require` and MCP is unreachable: **stop the run** and report the MCP failure.\n - If `mcp_mode=prefer` and MCP is unreachable: set `mcp_available=false`, continue, and apply confidence downgrades in the report assembly phase.\n\n**Step 3 — Common preflight** (always):\n\n8. Verify `{baseDir}/tools/generate_poc.py` exists and is executable. If missing: **stop the run** — PoC generation is mandatory.\n\n**Step 4 — Rust preflight** (skip if `language_mode=c`):\n\n9. Verify `cargo_manifest` file exists (must be a `Cargo.toml` path).\n10. Run `cargo check --manifest-path \u003ccargo_manifest>` to confirm the crate is buildable. If it fails: **stop the run**.\n11. Verify `cargo +nightly --version` succeeds. If not: **stop the run** — nightly toolchain is required for MIR and LLVM IR emission.\n - Note: use `~/.cargo/bin/cargo +nightly` (rustup proxy) rather than a system cargo that may not support the `+toolchain` syntax.\n12. Verify `uv --version` succeeds. If not: **stop the run** — `uv` is required to run Python analysis scripts.\n13. Verify `{baseDir}/tools/emit_rust_mir.sh` exists and is executable. If missing: **stop the run** — MIR analysis is required.\n14. Verify `{baseDir}/tools/emit_rust_ir.sh` exists and is executable. If missing: **stop the run** — LLVM IR analysis is required.\n15. For each tool below: if missing or not executable, warn and mark that capability as skipped (do not fail the run):\n - `{baseDir}/tools/emit_rust_asm.sh` — if missing, set `enable_asm=false` for Rust; warn `STACK_RETENTION`/`REGISTER_SPILL` findings will be skipped.\n - `{baseDir}/tools/diff_rust_mir.sh` — if missing, warn that MIR-level optimization comparison will be skipped.\n16. For each Python script below: if missing, warn and mark that sub-step as skipped (do not fail the run):\n - `{baseDir}/tools/scripts/semantic_audit.py`\n - `{baseDir}/tools/scripts/find_dangerous_apis.py`\n - `{baseDir}/tools/scripts/check_mir_patterns.py`\n - `{baseDir}/tools/scripts/check_llvm_patterns.py`\n - `{baseDir}/tools/scripts/check_rust_asm.py` — if missing, assembly analysis findings (`STACK_RETENTION`, `REGISTER_SPILL`) will be skipped even if `enable_asm=true`.\n\n**Step 5 — TU / crate enumeration**:\n\n- **C/C++** (skip if `language_mode=rust`): Parse `compile_db` and enumerate all translation units. Apply `max_tus` limit if set. Filter by `languages`. Compute `tu_hash = sha1(source_path)[:8]` for each TU. Run a lightweight grep across TU sources for sensitive name patterns (from merged config) to produce `sensitive_candidates`.\n- **Rust** (skip if `language_mode=c`): Compute `rust_tu_hash = sha1(abspath(cargo_manifest))[:8]`. Set `rust_crate_root = dirname(cargo_manifest)`.\n\n**Step 6 — Create work directory**:\n\n```bash\nRUN_ID=$(date +%Y%m%d%H%M%S)\nWORKDIR=\"/tmp/zeroize-audit-${RUN_ID}\"\nmkdir -p \"${WORKDIR}\"/{mcp-evidence,source-analysis,compiler-analysis,rust-compiler-analysis,report,poc,tests,agent-inputs}\n```\n\n**Step 7 — Write `{workdir}/preflight.json`**:\n\n```json\n{\n \"run_id\": \"\u003cRUN_ID>\",\n \"timestamp\": \"\u003cISO-8601>\",\n \"repo\": \"\u003cpath>\",\n \"language_mode\": \"\u003cc|rust|mixed>\",\n \"compile_db\": \"\u003ccompile_db or null>\",\n \"cargo_manifest\": \"\u003ccargo_manifest or null>\",\n \"rust_crate_root\": \"\u003cdirname(cargo_manifest) or null>\",\n \"rust_tu_hash\": \"\u003chash or null>\",\n \"opt_levels\": [\"O0\", \"O1\", \"O2\"],\n \"mcp_mode\": \"\u003cmcp_mode>\",\n \"mcp_available\": true,\n \"enable_asm\": true,\n \"enable_semantic_ir\": false,\n \"enable_cfg\": false,\n \"enable_runtime_tests\": false,\n \"tu_count\": 0,\n \"tu_list\": [{\"file\": \"/path/to/file.c\", \"tu_hash\": \"a1b2c3d4\"}],\n \"sensitive_candidates\": [],\n \"tools_verified\": [\"uv\", \"cargo+nightly\"],\n \"notes\": \"\"\n}\n```\n\n**Step 8 — Write `{workdir}/orchestrator-state.json`** with the full state structure.\n\nReport each preflight failure with the specific check that failed and the remediation step.\n\n**After completion**: The agent's response includes the `workdir` path. Read `{workdir}/orchestrator-state.json` to initialize:\n\n- `workdir` — use for all subsequent phases\n- `routing.mcp_available` — MCP probe result\n- `routing.tu_count` — number of TUs to process\n- `key_file_paths.config` — path to merged config file\n\n## State Update\n\nThe `0-preflight` agent writes the initial `orchestrator-state.json`. No additional update needed by the orchestrator.\n\n## Error Handling\n\n| Failure | Behavior |\n|---|---|\n| Agent fails or times out | Stop the run, report failure |\n| Neither `compile_db` nor `cargo_manifest` provided | Stop the run |\n| Preflight validation fails | Stop the run (agent reports specific check and remediation) |\n| Config load fails | Stop the run |\n| Preflight tool check fails | Stop the run |\n| MCP unreachable + `mcp_mode=require` | Stop the run |\n| MCP unreachable + `mcp_mode=prefer` | Continue — `routing.mcp_available` will be `false` |\n| `cargo check` fails (Rust preflight) | Stop the run — crate must be buildable |\n| `cargo +nightly` not available (Rust preflight) | Stop the run — nightly required for MIR/IR emission |\n| `uv` not available (Rust preflight) | Stop the run — required for Python analysis scripts |\n| `emit_rust_asm.sh` missing (Rust preflight) | Warn, set `enable_asm=false` for Rust, continue |\n| Python script missing (Rust preflight) | Warn and skip that sub-step, continue |\n\n## Next Phase\n\nPhase 1 — Source Analysis\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7476,"content_sha256":"cdf85ce427c13ae11cd3d35460818cd4e834eee13a1a2f45861a6a7d1e93002b"},{"filename":"workflows/phase-1-source-analysis.md","content":"# Phase 1 — MCP Resolution and Source Analysis\n\n## Preconditions\n\n- Phase 0 complete: `orchestrator-state.json` exists with `phases.0.status = \"complete\"`\n- `{workdir}/preflight.json` exists\n- `{workdir}/merged-config.yaml` exists\n\n## Instructions\n\n### Wave 1 — MCP Resolver\n\nSkip if `mcp_mode=off` or `routing.mcp_available=false` or `language_mode=rust` (MCP is C/C++ only).\n\nWrite agent inputs to `{workdir}/agent-inputs/mcp-resolver.json`:\n\n```json\n{\n \"sensitive_candidates\": \"\u003cfrom preflight.json sensitive_candidates>\"\n}\n```\n\nSpawn agent `1-mcp-resolver` via `Task` with:\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `repo_root` | `{{path}}` |\n| `compile_db` | `{{compile_db}}` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `input_file` | `{workdir}/agent-inputs/mcp-resolver.json` |\n| `mcp_timeout_ms` | `{{mcp_timeout_ms}}` |\n\n**After completion**: Read `{workdir}/mcp-evidence/status.json`.\n\n- If `status=failed` and `mcp_mode=require`: **stop the run**.\n- If `status=failed` and `mcp_mode=prefer`: set `mcp_available=false`.\n- If `status=partial` or `status=success`: set `mcp_available=true`.\n\n### Wave 2a — Source Analyzer (C/C++ only)\n\nSkip if `language_mode=rust`.\n\nWrite agent inputs to `{workdir}/agent-inputs/source-analyzer.json`:\n\n```json\n{\n \"tu_list\": \"\u003cfrom preflight.json tu_list>\"\n}\n```\n\nSpawn agent `2-source-analyzer` via `Task` **in the same message as Wave 2b** (parallel launch):\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `repo_root` | `{{path}}` |\n| `compile_db` | `{{compile_db}}` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `input_file` | `{workdir}/agent-inputs/source-analyzer.json` |\n| `mcp_available` | Result from Wave 1 |\n| `languages` | `{{languages}}` |\n| `max_tus` | `{{max_tus}}` |\n\n### Wave 2b — Rust Source Analyzer (Rust only)\n\nSkip if `language_mode=c`.\n\nSpawn agent `2b-rust-source-analyzer` via `Task` **in the same message as Wave 2a** (parallel launch):\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `repo_root` | `{{path}}` |\n| `cargo_manifest` | `{{cargo_manifest}}` |\n| `rust_crate_root` | From `preflight.json` |\n| `rust_tu_hash` | From `preflight.json` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `baseDir` | `{baseDir}` |\n\nThe `2b-rust-source-analyzer` agent must:\n\n1. Attempt rustdoc JSON generation:\n ```bash\n cargo +nightly rustdoc --manifest-path \u003ccargo_manifest> \\\n --document-private-items -- -Z unstable-options --output-format json\n ```\n If this fails, warn and skip — proceed with source grep only.\n2. Run semantic audit (if rustdoc JSON succeeded):\n ```bash\n uv run {baseDir}/tools/scripts/semantic_audit.py \\\n --rustdoc target/doc/\u003ccrate>.json \\\n --cargo-toml \u003ccargo_manifest> \\\n --out {workdir}/source-analysis/rust-semantic-findings.json\n ```\n3. Run dangerous API scan:\n ```bash\n uv run {baseDir}/tools/scripts/find_dangerous_apis.py \\\n --src \u003crust_crate_root>/src \\\n --out {workdir}/source-analysis/rust-dangerous-api-findings.json\n ```\n4. Merge outputs into `{workdir}/source-analysis/sensitive-objects.json` (Rust `SO-NNNN` IDs with offset 5000+), `{workdir}/source-analysis/source-findings.json` (IDs `F-RUST-SRC-NNNN`), and `{workdir}/source-analysis/tu-map.json` (adding `{\"\u003ccargo_manifest>\": \"\u003crust_tu_hash>\"}`).\n5. Write `{workdir}/source-analysis/rust-notes.md` summarizing findings and any skipped steps.\n\n**After both Wave 2a and Wave 2b complete**: Read `{workdir}/source-analysis/tu-map.json`.\n\n- If empty (`{}`): no sensitive objects found. Skip to Phase 6 (empty report).\n- Determine entry classes in `tu-map.json`:\n - **C/C++ entry**: key is a source file path from `compile_commands.json` (typically `.c`, `.cc`, `.cpp`, `.cxx`).\n - **Rust entry**: key is the `cargo_manifest` path (`.../Cargo.toml`).\n- If no C/C++ entries: skip Wave 3 in Phase 2.\n- If no Rust entry: skip Wave 3R in Phase 2.\n- Otherwise: proceed to Phase 2.\n\n## State Update\n\nUpdate `orchestrator-state.json`:\n\n```json\n{\n \"current_phase\": 1,\n \"routing\": {\n \"mcp_available\": \"\u003cupdated value>\",\n \"tu_count\": \"\u003ccount of TUs in tu-map.json>\"\n },\n \"phases\": {\n \"1\": {\"status\": \"complete\", \"output\": \"source-analysis/tu-map.json\"}\n }\n}\n```\n\n## Error Handling\n\n| Failure | Behavior |\n|---|---|\n| MCP resolver fails + `mcp_mode=require` | Stop the run |\n| MCP resolver fails + `mcp_mode=prefer` | Continue with `mcp_available=false` |\n| Source analyzer (C/C++) fails | Stop C/C++ analysis — no sensitive object list for C/C++ TUs |\n| Rust source analyzer fails | Stop Rust analysis — log failure, continue if C/C++ analysis is also running |\n| No sensitive objects found | Skip Phases 2–5, jump to Phase 6 for empty report |\n\n## Next Phase\n\nPhase 2 — Compiler Analysis (if `tu-map.json` is non-empty)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4814,"content_sha256":"23c0b46eb0556d680762793a756b49ad025ac559a7d8f51b6f2a2a2f25e45a98"},{"filename":"workflows/phase-2-compiler-analysis.md","content":"# Phase 2 — Compiler Analysis\n\n## Preconditions\n\n- Phase 1 complete: `tu-map.json` is non-empty\n- `{workdir}/source-analysis/sensitive-objects.json` exists\n- `{workdir}/source-analysis/source-findings.json` exists\n\n## Instructions\n\n### Wave 3 — TU Compiler Analyzers (C/C++ only, N parallel)\n\nSkip if `language_mode=rust` or `tu-map.json` has no C/C++ entries.\n\nFor each C/C++ TU in `{workdir}/source-analysis/tu-map.json`:\n\n1. Create output directory:\n ```bash\n mkdir -p {workdir}/compiler-analysis/\u003ctu_hash>\n ```\n\n2. Write per-TU agent input to `{workdir}/agent-inputs/tu-\u003ctu_hash>.json`:\n ```json\n {\n \"sensitive_objects\": \"\u003csubset of sensitive-objects.json matching this TU>\",\n \"source_findings\": \"\u003csubset of source-findings.json matching this TU>\"\n }\n ```\n\n3. Spawn agent `3-tu-compiler-analyzer` via `Task` with:\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `tu_source` | Source file path (from tu-map key) |\n| `tu_hash` | TU hash (from tu-map value) |\n| `compile_db` | `{{compile_db}}` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `input_file` | `{workdir}/agent-inputs/tu-\u003ctu_hash>.json` |\n| `opt_levels` | `{{opt_levels}}` |\n| `enable_asm` | `{{enable_asm}}` |\n| `enable_semantic_ir` | `{{enable_semantic_ir}}` |\n| `enable_cfg` | `{{enable_cfg}}` |\n| `baseDir` | `{baseDir}` |\n\nLaunch TU agents in parallel using multiple `Task` calls in a single message. **Batching**: if the TU count exceeds 15, launch in batches of 10–15; wait for each batch before launching the next.\n\n**After all TU agents complete**: Verify `{workdir}/compiler-analysis/\u003ctu_hash>/ir-findings.json` exists for each TU. Log any failed TUs but continue.\n\n### Wave 3R — Rust Compiler Analyzer (single agent)\n\nSkip if any of the following are true:\n- `language_mode=c`\n- `tu-map.json` has no Rust entry (manifest key `.../Cargo.toml`)\n- `sensitive-objects.json` is missing or empty\n- `sensitive-objects.json` has no Rust objects (IDs `SO-5NNN` / `SO-5000+`)\n\nSpawn agent `3b-rust-compiler-analyzer` via `Task` (after Wave 3 completes or is skipped):\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `cargo_manifest` | `{{cargo_manifest}}` |\n| `rust_crate_root` | From `preflight.json` |\n| `rust_tu_hash` | From `preflight.json` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `opt_levels` | `{{opt_levels}}` |\n| `enable_asm` | `{{enable_asm}}` |\n| `input_file` | `{workdir}/agent-inputs/rust-compiler.json` (write Rust-subset of sensitive-objects and source-findings before spawn) |\n| `baseDir` | `{baseDir}` |\n\nThe `3b-rust-compiler-analyzer` agent must run these steps in order. On step failures, write status-bearing error objects to the affected output file(s) and continue.\n\n**Step A — MIR analysis:**\n```bash\n{baseDir}/tools/emit_rust_mir.sh --manifest \u003ccargo_manifest> --lib --opt O0 \\\n --out {workdir}/rust-compiler-analysis/\u003crust_tu_hash>.mir\nuv run {baseDir}/tools/scripts/check_mir_patterns.py \\\n --mir {workdir}/rust-compiler-analysis/\u003crust_tu_hash>.mir \\\n --secrets {workdir}/source-analysis/sensitive-objects.json \\\n --out {workdir}/rust-compiler-analysis/mir-findings.json\n```\n\n**Step B — LLVM IR analysis (O0 vs O2):**\n```bash\n{baseDir}/tools/emit_rust_ir.sh --manifest \u003ccargo_manifest> --lib --opt O0 \\\n --out {workdir}/rust-compiler-analysis/\u003crust_tu_hash>.O0.ll\n{baseDir}/tools/emit_rust_ir.sh --manifest \u003ccargo_manifest> --lib --opt O2 \\\n --out {workdir}/rust-compiler-analysis/\u003crust_tu_hash>.O2.ll\nuv run {baseDir}/tools/scripts/check_llvm_patterns.py \\\n --o0 {workdir}/rust-compiler-analysis/\u003crust_tu_hash>.O0.ll \\\n --o2 {workdir}/rust-compiler-analysis/\u003crust_tu_hash>.O2.ll \\\n --out {workdir}/rust-compiler-analysis/ir-findings.json\n```\n\n**Step C — Assembly analysis** (skip if `enable_asm=false` or `emit_rust_asm.sh` missing):\n```bash\n{baseDir}/tools/emit_rust_asm.sh --manifest \u003ccargo_manifest> --lib --opt O2 \\\n --out {workdir}/rust-compiler-analysis/\u003crust_tu_hash>.O2.s\nuv run {baseDir}/tools/scripts/check_rust_asm.py \\\n --asm {workdir}/rust-compiler-analysis/\u003crust_tu_hash>.O2.s \\\n --secrets {workdir}/source-analysis/sensitive-objects.json \\\n --out {workdir}/rust-compiler-analysis/asm-findings.json\n```\n\nIf assembly tools are missing, write `[]` to `asm-findings.json`.\n\nIR finding IDs: `F-RUST-IR-NNNN`. MIR finding IDs: `F-RUST-MIR-NNNN`. Assembly finding IDs: `F-RUST-ASM-NNNN`.\n\nWrite `{workdir}/rust-compiler-analysis/notes.md` summarizing all steps, any failures, and key observations.\n\n**After Wave 3R completes**: Verify `mir-findings.json`, `ir-findings.json`, and `asm-findings.json` exist under `{workdir}/rust-compiler-analysis/`. Log if missing, continue.\n\n## State Update\n\nUpdate `orchestrator-state.json`:\n\n```json\n{\n \"current_phase\": 2,\n \"phases\": {\n \"2\": {\"status\": \"complete\", \"tus_succeeded\": \"\u003cN>\", \"tus_failed\": \"\u003cN>\"}\n }\n}\n```\n\n## Error Handling\n\n| Failure | Behavior |\n|---|---|\n| One TU agent (C/C++) fails | Continue with remaining TUs |\n| All TU agents (C/C++) fail | Proceed — report assembler produces source-only report |\n| Rust compiler analyzer (Wave 3R) fails | Log failure, continue — report assembler handles missing `rust-compiler-analysis/` |\n| `emit_rust_asm.sh` missing | Write `[]` to `asm-findings.json`, continue — assembly findings skipped |\n| MIR or IR emission fails | Write `[]` to that step's output, continue with remaining steps |\n\n## Next Phase\n\nPhase 3 — Interim Report\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5441,"content_sha256":"9ac1b095f7966fb985279c963be27fa7c43031c1348893bc91968fe120f24731"},{"filename":"workflows/phase-3-interim-report.md","content":"# Phase 3 — Interim Finding Collection\n\n## Preconditions\n\n- Phase 2 complete (or skipped if no compiler analysis needed)\n\n## Instructions\n\nSpawn agent `4-report-assembler` via `Task` with:\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `mcp_available` | From `orchestrator-state.json` routing |\n| `mcp_required_for_advanced` | `{{mcp_required_for_advanced}}` |\n| `baseDir` | `{baseDir}` |\n| `mode` | `interim` |\n\n**After completion**: Verify `{workdir}/report/findings.json` exists. Count findings. If the findings array is empty, skip to Phase 6 for an empty report.\n\n## State Update\n\nUpdate `orchestrator-state.json`:\n\n```json\n{\n \"current_phase\": 3,\n \"routing\": {\n \"finding_count\": \"\u003ccount from findings.json>\"\n },\n \"phases\": {\n \"3\": {\"status\": \"complete\", \"output\": \"report/findings.json\"}\n }\n}\n```\n\n## Error Handling\n\n| Failure | Behavior |\n|---|---|\n| Report assembler fails | Surface error to user |\n\n## Next Phase\n\nPhase 4 — PoC Generation (if `finding_count > 0`)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1054,"content_sha256":"f898af293481782908c4d556ccd511ca190740a72a75258283ff03ee6e1df024"},{"filename":"workflows/phase-4-poc-generation.md","content":"# Phase 4 — PoC Generation\n\n## Preconditions\n\n- Phase 3 complete: `{workdir}/report/findings.json` exists with at least one finding\n\n## Instructions\n\nSpawn agent `5-poc-generator` via `Task` with:\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `compile_db` | `{{compile_db}}` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `final_report` | `{workdir}/report/findings.json` |\n| `poc_categories` | `{{poc_categories}}` |\n| `poc_output_dir` | `{{poc_output_dir}}` or `{workdir}/poc/` |\n| `baseDir` | `{baseDir}` |\n\nThe agent reads each finding and the corresponding source code, then crafts a bespoke PoC program tailored to the specific vulnerability. Each PoC is individually written — not generated from templates.\n\n**After completion**: Verify `{workdir}/poc/poc_manifest.json` exists and contains an entry for each finding.\n\n## State Update\n\nUpdate `orchestrator-state.json`:\n\n```json\n{\n \"current_phase\": 4,\n \"phases\": {\n \"4\": {\"status\": \"complete\", \"output\": \"poc/poc_manifest.json\"}\n }\n}\n```\n\n## Error Handling\n\n| Failure | Behavior |\n|---|---|\n| PoC generator fails | Pipeline stalls — surface error to user |\n\n## Next Phase\n\nPhase 5 — PoC Validation & Verification\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1209,"content_sha256":"2f7a0daf35c60de699de92c5f6e98555f51302aaa92e80c619ef1dcd86ccff01"},{"filename":"workflows/phase-5-poc-validation.md","content":"# Phase 5 — PoC Validation & Verification\n\n## Preconditions\n\n- Phase 4 complete: `{workdir}/poc/poc_manifest.json` exists\n\n## Instructions\n\n### Step 5a — Compile and Run All PoCs (agent)\n\nSpawn agent `5b-poc-validator` via `Task` with:\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n\n**After completion**: Read `{workdir}/poc/poc_validation_results.json`.\n\nIf the agent fails, fall back to compiling and running PoCs inline:\n\n```bash\ncd {workdir}/poc && make \u003cmakefile_target>\n./\u003cmakefile_target>\necho \"Exit code: $?\"\n```\n\n### Step 5b — Verify PoCs Prove Their Claims (agent)\n\nSpawn agent `5c-poc-verifier` via `Task` with:\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `validation_results` | `{workdir}/poc/poc_validation_results.json` |\n\nThe verifier reads each PoC source file, the corresponding finding, and the original source code to check that the PoC actually tests the claimed vulnerability. It verifies:\n- Target variable and function match the finding\n- Verification technique is appropriate for the finding category\n- Optimization level is correct\n- Exit code interpretation is not inverted\n- Results are plausible given the finding evidence\n\n**After completion**: Read `{workdir}/poc/poc_verification.json`.\n\n### Step 5c — Present Verification Failures to User\n\nRead `{workdir}/poc/poc_verification.json`. For any PoC with `verified: false`:\n\n1. Use `Read` to show the PoC source file.\n2. Present to the user via `AskUserQuestion` with:\n - Finding ID and category\n - PoC file path\n - Which verification checks failed and why\n - The verifier's notes\n - The PoC's runtime result (from `poc_validation_results.json`)\n\n3. Ask the user whether to:\n - **Accept anyway**: Trust the PoC result despite verification failure\n - **Reject**: Discard the PoC result (treat as `no_poc` for this finding)\n\n**Block until the user responds for each failed PoC.**\n\n### Step 5d — Merge Results\n\nCombine validation results (from `poc_validation_results.json`), verification results (from `poc_verification.json`), and user decisions (from Step 5c).\n\nWrite `{workdir}/poc/poc_final_results.json`:\n\n```json\n{\n \"timestamp\": \"\u003cISO-8601>\",\n \"results\": [\n {\n \"finding_id\": \"ZA-0001\",\n \"category\": \"MISSING_SOURCE_ZEROIZE\",\n \"poc_file\": \"poc_za_0001_missing_source_zeroize.c\",\n \"compile_success\": true,\n \"exit_code\": 0,\n \"validation_result\": \"exploitable\",\n \"verification\": {\n \"verified\": true,\n \"checks\": { \"...\": \"pass\" },\n \"notes\": \"PoC correctly targets session_key in handle_key()\"\n }\n },\n {\n \"finding_id\": \"ZA-0003\",\n \"category\": \"OPTIMIZED_AWAY_ZEROIZE\",\n \"poc_file\": \"poc_za_0003_optimized_away_zeroize.c\",\n \"compile_success\": true,\n \"exit_code\": 1,\n \"validation_result\": \"rejected\",\n \"verification\": {\n \"verified\": false,\n \"checks\": { \"optimization_level\": \"fail\" },\n \"notes\": \"Compiled at -O0 but wipe disappears at -O2. User rejected PoC result.\"\n }\n }\n ]\n}\n```\n\nValidation result mapping:\n\n- `compile_success=true, exit_code=0, verified=true` → `\"exploitable\"`\n- `compile_success=true, exit_code=1, verified=true` → `\"not_exploitable\"`\n- `compile_success=true, verified=false, user accepted` → original result (`\"exploitable\"` or `\"not_exploitable\"`)\n- `compile_success=true, verified=false, user rejected` → `\"rejected\"`\n- `compile_success=false` → `\"compile_failure\"`\n\n## State Update\n\nUpdate `orchestrator-state.json`:\n\n```json\n{\n \"current_phase\": 5,\n \"phases\": {\n \"5\": {\"status\": \"complete\", \"output\": \"poc/poc_final_results.json\"}\n }\n}\n```\n\n## Error Handling\n\n| Failure | Behavior |\n|---|---|\n| Validator agent fails | Fall back to inline compilation for all PoCs |\n| Verifier agent fails | Skip verification, use validation results only (warn in report) |\n| Individual PoC compile failure | Record in results, continue with others |\n\n## Next Phase\n\nPhase 6 — Final Report\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4088,"content_sha256":"0cd58c2b9b92bab27303af0c78d283d06094b1004ab823641f311c66e3133c5e"},{"filename":"workflows/phase-6-final-report.md","content":"# Phase 6 — Report Finalization\n\n## Preconditions\n\n- Phase 5 complete (or skipped if zero findings): `poc_final_results.json` exists or findings are empty\n\n## Instructions\n\nSpawn agent `4-report-assembler` via `Task` with:\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `mcp_available` | From `orchestrator-state.json` routing |\n| `mcp_required_for_advanced` | `{{mcp_required_for_advanced}}` |\n| `baseDir` | `{baseDir}` |\n| `mode` | `final` |\n| `poc_results` | `{workdir}/poc/poc_final_results.json` |\n\n**After completion**: Verify `{workdir}/report/final-report.md` and updated `{workdir}/report/findings.json` exist.\n\n## State Update\n\nUpdate `orchestrator-state.json`:\n\n```json\n{\n \"current_phase\": 6,\n \"phases\": {\n \"6\": {\"status\": \"complete\", \"output\": \"report/final-report.md\"}\n }\n}\n```\n\n## Error Handling\n\n| Failure | Behavior |\n|---|---|\n| Report assembler fails | Surface error to user |\n\n## Next Phase\n\nPhase 7 — Test Generation (if `enable_runtime_tests=true` and `finding_count > 0`)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1071,"content_sha256":"9a318c4d9bd2c66e183d2a48a0cdef41e8f7e22d721bdaa4cf766831a7f48af7"},{"filename":"workflows/phase-7-test-generation.md","content":"# Phase 7 — Test Generation\n\n## Preconditions\n\n- Phase 6 complete\n- `enable_runtime_tests=true`\n- Finding count > 0\n\n## Instructions\n\nSpawn agent `6-test-generator` via `Task` with:\n\n| Parameter | Value |\n|---|---|\n| `workdir` | `{workdir}` |\n| `compile_db` | `{{compile_db}}` |\n| `config_path` | `{workdir}/merged-config.yaml` |\n| `final_report` | `{workdir}/report/findings.json` |\n| `baseDir` | `{baseDir}` |\n\n## State Update\n\nUpdate `orchestrator-state.json`:\n\n```json\n{\n \"current_phase\": 7,\n \"phases\": {\n \"7\": {\"status\": \"complete\", \"output\": \"tests/\"}\n }\n}\n```\n\n## Error Handling\n\n| Failure | Behavior |\n|---|---|\n| Test generator fails | Report is still available without tests |\n\n## Next Phase\n\nPhase 8 — Return Results (handled inline by dispatcher)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":769,"content_sha256":"3729993bd51803417139e539f09bb07cbba3b541c44087492c1dd596a84ef2c0"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"zeroize-audit — Claude Skill","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Auditing cryptographic implementations (keys, seeds, nonces, secrets)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reviewing authentication systems (passwords, tokens, session data)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Analyzing code that handles PII or sensitive credentials","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verifying secure cleanup in security-critical codebases","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Investigating memory safety of sensitive data handling","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When NOT to Use","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"General code review without security focus","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Performance optimization (unless related to secure wiping)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Refactoring tasks not related to sensitive data","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Code without identifiable secrets or sensitive values","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Purpose","type":"text"}]},{"type":"paragraph","content":[{"text":"Detect missing zeroization of sensitive data in source code and identify zeroization that is removed or weakened by compiler optimizations (e.g., dead-store elimination), with mandatory LLVM IR/asm evidence. Capabilities include:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Assembly-level analysis for register spills and stack retention","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data-flow tracking for secret copies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Heap allocator security warnings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Semantic IR analysis for loop unrolling and SSA form","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Control-flow graph analysis for path coverage verification","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Runtime validation test generation","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Scope","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read-only against the target codebase (does not modify audited code; writes analysis artifacts to a temporary working directory).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Produces a structured report (JSON).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Requires valid build context (","type":"text"},{"text":"compile_commands.json","type":"text","marks":[{"type":"code_inline"}]},{"text":") and compilable translation units.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Optimized away\" findings only allowed with compiler evidence (IR/asm diff).","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Inputs","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"{baseDir}/schemas/input.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full schema. Key fields:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"path","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Repo root","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"compile_db","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"null","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Path to ","type":"text"},{"text":"compile_commands.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" for C/C++ analysis. Required if ","type":"text"},{"text":"cargo_manifest","type":"text","marks":[{"type":"code_inline"}]},{"text":" is not set.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cargo_manifest","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"null","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Path to ","type":"text"},{"text":"Cargo.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":" for Rust crate analysis. Required if ","type":"text"},{"text":"compile_db","type":"text","marks":[{"type":"code_inline"}]},{"text":" is not set.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"config","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"YAML defining heuristics and approved wipes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"opt_levels","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"[\"O0\",\"O1\",\"O2\"]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Optimization levels for IR comparison. O1 is the diagnostic level: if a wipe disappears at O1 it is simple DSE; O2 catches more aggressive eliminations.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"languages","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"[\"c\",\"cpp\",\"rust\"]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Languages to analyze","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"max_tus","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Limit on translation units processed from compile DB","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mcp_mode","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"prefer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"off","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"prefer","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"require","type":"text","marks":[{"type":"code_inline"}]},{"text":" — controls Serena MCP usage","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mcp_required_for_advanced","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"true","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Downgrade ","type":"text"},{"text":"SECRET_COPY","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"MISSING_ON_ERROR_PATH","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"NOT_DOMINATING_EXITS","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"needs_review","type":"text","marks":[{"type":"code_inline"}]},{"text":" when MCP is unavailable","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mcp_timeout_ms","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Timeout budget for MCP semantic queries","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"poc_categories","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"all 11 exploitable","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Finding categories for which to generate PoCs. C/C++ findings: all 11 categories supported. Rust findings: only ","type":"text"},{"text":"MISSING_SOURCE_ZEROIZE","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"SECRET_COPY","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"PARTIAL_WIPE","type":"text","marks":[{"type":"code_inline"}]},{"text":" are supported; other Rust categories are marked ","type":"text"},{"text":"poc_supported=false","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"poc_output_dir","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"generated_pocs/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output directory for generated PoCs","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"enable_asm","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"true","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enable assembly emission and analysis (Step 8); produces ","type":"text"},{"text":"STACK_RETENTION","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"REGISTER_SPILL","type":"text","marks":[{"type":"code_inline"}]},{"text":". Auto-disabled if ","type":"text"},{"text":"emit_asm.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" is missing.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"enable_semantic_ir","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"false","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enable semantic LLVM IR analysis (Step 9); produces ","type":"text"},{"text":"LOOP_UNROLLED_INCOMPLETE","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"enable_cfg","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"false","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enable control-flow graph analysis (Step 10); produces ","type":"text"},{"text":"MISSING_ON_ERROR_PATH","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"NOT_DOMINATING_EXITS","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"enable_runtime_tests","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"no","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"false","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enable runtime test harness generation (Step 11)","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"paragraph","content":[{"text":"Before running, verify the following. Each has a defined failure mode.","type":"text"}]},{"type":"paragraph","content":[{"text":"C/C++ prerequisites:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prerequisite","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Failure mode if missing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"compile_commands.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" at ","type":"text"},{"text":"compile_db","type":"text","marks":[{"type":"code_inline"}]},{"text":" path","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — do not proceed","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"clang","type":"text","marks":[{"type":"code_inline"}]},{"text":" on PATH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — IR/ASM analysis impossible","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"uvx","type":"text","marks":[{"type":"code_inline"}]},{"text":" on PATH (for Serena)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"mcp_mode=require","type":"text","marks":[{"type":"code_inline"}]},{"text":": fail. If ","type":"text"},{"text":"mcp_mode=prefer","type":"text","marks":[{"type":"code_inline"}]},{"text":": continue without MCP; downgrade affected findings per Confidence Gating rules.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/extract_compile_flags.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — cannot extract per-TU flags","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/emit_ir.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — IR analysis impossible","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/emit_asm.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and skip assembly findings (STACK_RETENTION, REGISTER_SPILL)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/mcp/check_mcp.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and treat as MCP unavailable","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/mcp/normalize_mcp_evidence.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and use raw MCP output","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Rust prerequisites:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prerequisite","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Failure mode if missing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cargo.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":" at ","type":"text"},{"text":"cargo_manifest","type":"text","marks":[{"type":"code_inline"}]},{"text":" path","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — do not proceed","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cargo check","type":"text","marks":[{"type":"code_inline"}]},{"text":" passes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — crate must be buildable","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cargo +nightly","type":"text","marks":[{"type":"code_inline"}]},{"text":" on PATH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — nightly required for MIR and LLVM IR emission","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"uv","type":"text","marks":[{"type":"code_inline"}]},{"text":" on PATH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — required to run Python analysis scripts","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/validate_rust_toolchain.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn — run preflight manually. Checks all tools, scripts, nightly, and optionally ","type":"text"},{"text":"cargo check","type":"text","marks":[{"type":"code_inline"}]},{"text":". Use ","type":"text"},{"text":"--json","type":"text","marks":[{"type":"code_inline"}]},{"text":" for machine-readable output, ","type":"text"},{"text":"--manifest","type":"text","marks":[{"type":"code_inline"}]},{"text":" to also validate the crate builds.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/emit_rust_mir.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — MIR analysis impossible (","type":"text"},{"text":"--opt","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--crate","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--bin/--lib","type":"text","marks":[{"type":"code_inline"}]},{"text":" supported; ","type":"text"},{"text":"--out","type":"text","marks":[{"type":"code_inline"}]},{"text":" can be file or directory)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/emit_rust_ir.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — LLVM IR analysis impossible (","type":"text"},{"text":"--opt","type":"text","marks":[{"type":"code_inline"}]},{"text":" required; ","type":"text"},{"text":"--crate","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--bin/--lib","type":"text","marks":[{"type":"code_inline"}]},{"text":" supported; ","type":"text"},{"text":"--out","type":"text","marks":[{"type":"code_inline"}]},{"text":" must be ","type":"text"},{"text":".ll","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/emit_rust_asm.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and skip assembly findings (","type":"text"},{"text":"STACK_RETENTION","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"REGISTER_SPILL","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Supports ","type":"text"},{"text":"--opt","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--crate","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--bin/--lib","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--target","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--intel-syntax","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"--out","type":"text","marks":[{"type":"code_inline"}]},{"text":" can be ","type":"text"},{"text":".s","type":"text","marks":[{"type":"code_inline"}]},{"text":" file or directory.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/diff_rust_mir.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and skip MIR-level optimization comparison. Accepts 2+ MIR files, normalizes, diffs pairwise, and reports first opt level where zeroize/drop-glue patterns disappear.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/scripts/semantic_audit.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and skip semantic source analysis","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/scripts/find_dangerous_apis.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and skip dangerous API scan","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/scripts/check_mir_patterns.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and skip MIR analysis","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/scripts/check_llvm_patterns.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and skip LLVM IR analysis","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/scripts/check_rust_asm.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and skip Rust assembly analysis (","type":"text"},{"text":"STACK_RETENTION","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"REGISTER_SPILL","type":"text","marks":[{"type":"code_inline"}]},{"text":", drop-glue checks). Dispatches to ","type":"text"},{"text":"check_rust_asm_x86.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" (production) or ","type":"text"},{"text":"check_rust_asm_aarch64.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"EXPERIMENTAL","type":"text","marks":[{"type":"strong"}]},{"text":" — AArch64 findings require manual verification).","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/scripts/check_rust_asm_x86.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required by ","type":"text"},{"text":"check_rust_asm.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" for x86-64 analysis; warn and skip if missing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/scripts/check_rust_asm_aarch64.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required by ","type":"text"},{"text":"check_rust_asm.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" for AArch64 analysis (","type":"text"},{"text":"EXPERIMENTAL","type":"text","marks":[{"type":"strong"}]},{"text":"); warn and skip if missing","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Common prerequisite:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prerequisite","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Failure mode if missing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{baseDir}/tools/generate_poc.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fail fast — PoC generation is mandatory","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Approved Wipe APIs","type":"text"}]},{"type":"paragraph","content":[{"text":"The following are recognized as valid zeroization. Configure additional entries in ","type":"text"},{"text":"{baseDir}/configs/","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"C/C++","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"explicit_bzero","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"memset_s","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SecureZeroMemory","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OPENSSL_cleanse","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sodium_memzero","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Volatile wipe loops (pattern-based; see ","type":"text"},{"text":"volatile_wipe_patterns","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":"{baseDir}/configs/default.yaml","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"In IR: ","type":"text"},{"text":"llvm.memset","type":"text","marks":[{"type":"code_inline"}]},{"text":" with volatile flag, volatile stores, or non-elidable wipe call","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Rust","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"zeroize::Zeroize","type":"text","marks":[{"type":"code_inline"}]},{"text":" trait (","type":"text"},{"text":"zeroize()","type":"text","marks":[{"type":"code_inline"}]},{"text":" method)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Zeroizing\u003cT>","type":"text","marks":[{"type":"code_inline"}]},{"text":" wrapper (drop-based)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ZeroizeOnDrop","type":"text","marks":[{"type":"code_inline"}]},{"text":" derive macro","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Finding Capabilities","type":"text"}]},{"type":"paragraph","content":[{"text":"Findings are grouped by required evidence. Only attempt findings for which the required tooling is available.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Finding ID","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Requires","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PoC Support","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MISSING_SOURCE_ZEROIZE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No zeroization found in source","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source only","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (C/C++ + Rust)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PARTIAL_WIPE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Incorrect size or incomplete wipe","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source only","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (C/C++ + Rust)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"NOT_ON_ALL_PATHS","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Zeroization missing on some control-flow paths (heuristic)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source only","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (C/C++ only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SECRET_COPY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sensitive data copied without zeroization tracking","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source + MCP preferred","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (C/C++ + Rust)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"INSECURE_HEAP_ALLOC","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Secret uses insecure allocator (malloc vs. secure_malloc)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source only","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (C/C++ only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OPTIMIZED_AWAY_ZEROIZE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Compiler removed zeroization","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"IR diff required (never source-only)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"STACK_RETENTION","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stack frame may retain secrets after return","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Assembly required (C/C++); LLVM IR ","type":"text"},{"text":"alloca","type":"text","marks":[{"type":"code_inline"}]},{"text":"+","type":"text"},{"text":"lifetime.end","type":"text","marks":[{"type":"code_inline"}]},{"text":" evidence (Rust); assembly corroboration upgrades to ","type":"text"},{"text":"confirmed","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (C/C++ only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"REGISTER_SPILL","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Secrets spilled from registers to stack","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Assembly required (C/C++); LLVM IR ","type":"text"},{"text":"load","type":"text","marks":[{"type":"code_inline"}]},{"text":"+call-site evidence (Rust); assembly corroboration upgrades to ","type":"text"},{"text":"confirmed","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (C/C++ only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MISSING_ON_ERROR_PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error-handling paths lack cleanup","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CFG or MCP required","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"NOT_DOMINATING_EXITS","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wipe doesn't dominate all exits","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CFG or MCP required","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LOOP_UNROLLED_INCOMPLETE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Unrolled loop wipe is incomplete","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Semantic IR required","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Agent Architecture","type":"text"}]},{"type":"paragraph","content":[{"text":"The analysis pipeline uses 11 agents across 8 phases, invoked by the orchestrator (","type":"text"},{"text":"{baseDir}/prompts/task.md","type":"text","marks":[{"type":"code_inline"}]},{"text":") via ","type":"text"},{"text":"Task","type":"text","marks":[{"type":"code_inline"}]},{"text":". Agents write persistent finding files to a shared working directory (","type":"text"},{"text":"/tmp/zeroize-audit-{run_id}/","type":"text","marks":[{"type":"code_inline"}]},{"text":"), enabling parallel execution and protecting against context pressure.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Agent","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output Directory","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0-preflight","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 0","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Preflight checks (tools, toolchain, compile DB, crate build), config merge, workdir creation, TU enumeration","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{workdir}/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1-mcp-resolver","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 1, Wave 1 (C/C++ only)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resolve symbols, types, and cross-file references via Serena MCP","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mcp-evidence/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2-source-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 1, Wave 2a (C/C++ only)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Identify sensitive objects, detect wipes, validate correctness, data-flow/heap","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source-analysis/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2b-rust-source-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 1, Wave 2b (Rust only, parallel with 2a)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rustdoc JSON trait-aware analysis + dangerous API grep","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source-analysis/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3-tu-compiler-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 2, Wave 3 (C/C++ only, N parallel)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Per-TU IR diff, assembly, semantic IR, CFG analysis","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"compiler-analysis/{tu_hash}/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3b-rust-compiler-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 2, Wave 3R (Rust only, single agent)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Crate-level MIR, LLVM IR, and assembly analysis","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rust-compiler-analysis/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4-report-assembler","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 3 (interim) + Phase 6 (final)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Collect findings from all agents, apply confidence gates; merge PoC results and produce final report","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"report/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5-poc-generator","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Craft bespoke proof-of-concept programs (C/C++: all categories; Rust: MISSING_SOURCE_ZEROIZE, SECRET_COPY, PARTIAL_WIPE)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"poc/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5b-poc-validator","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Compile and run all PoCs","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"poc/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5c-poc-verifier","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verify each PoC proves its claimed finding","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"poc/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6-test-generator","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 7 (optional)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate runtime validation test harnesses","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"tests/","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"The orchestrator reads one per-phase workflow file from ","type":"text"},{"text":"{baseDir}/workflows/","type":"text","marks":[{"type":"code_inline"}]},{"text":" at a time, and maintains ","type":"text"},{"text":"orchestrator-state.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" for recovery after context compression. Agents receive configuration by file path (","type":"text"},{"text":"config_path","type":"text","marks":[{"type":"code_inline"}]},{"text":"), not by value.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Execution flow","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Phase 0: 0-preflight agent — Preflight + config + create workdir + enumerate TUs\n → writes orchestrator-state.json, merged-config.yaml, preflight.json\nPhase 1: Wave 1: 1-mcp-resolver (skip if mcp_mode=off OR language_mode=rust)\n Wave 2a: 2-source-analyzer (C/C++ only; skip if no compile_db) ─┐ parallel\n Wave 2b: 2b-rust-source-analyzer (Rust only; skip if no cargo_manifest) ─┘\nPhase 2: Wave 3: 3-tu-compiler-analyzer x N (C/C++ only; parallel per TU)\n Wave 3R: 3b-rust-compiler-analyzer (Rust only; single crate-level agent)\nPhase 3: Wave 4: 4-report-assembler (mode=interim → findings.json; reads all agent outputs)\nPhase 4: Wave 5: 5-poc-generator (C/C++: all categories; Rust: MISSING_SOURCE_ZEROIZE, SECRET_COPY, PARTIAL_WIPE; other Rust findings: poc_supported=false)\nPhase 5: PoC Validation & Verification\n Step 1: 5b-poc-validator agent (compile and run all PoCs)\n Step 2: 5c-poc-verifier agent (verify each PoC proves its claimed finding)\n Step 3: Orchestrator presents verification failures to user via AskUserQuestion\n Step 4: Orchestrator merges all results into poc_final_results.json\nPhase 6: Wave 6: 4-report-assembler (mode=final → merge PoC results, final-report.md)\nPhase 7: Wave 7: 6-test-generator (optional)\nPhase 8: Orchestrator — Return final-report.md","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cross-Reference Convention","type":"text"}]},{"type":"paragraph","content":[{"text":"IDs are namespaced per agent to prevent collisions during parallel execution:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Entity","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pattern","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Assigned By","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sensitive object (C/C++)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SO-0001","type":"text","marks":[{"type":"code_inline"}]},{"text":"–","type":"text"},{"text":"SO-4999","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2-source-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sensitive object (Rust)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SO-5000","type":"text","marks":[{"type":"code_inline"}]},{"text":"–","type":"text"},{"text":"SO-9999","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Rust namespace)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2b-rust-source-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source finding (C/C++)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"F-SRC-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2-source-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source finding (Rust)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"F-RUST-SRC-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2b-rust-source-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"IR finding (C/C++)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"F-IR-{tu_hash}-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3-tu-compiler-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ASM finding (C/C++)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"F-ASM-{tu_hash}-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3-tu-compiler-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CFG finding","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"F-CFG-{tu_hash}-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3-tu-compiler-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Semantic IR finding","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"F-SIR-{tu_hash}-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3-tu-compiler-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rust MIR finding","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"F-RUST-MIR-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3b-rust-compiler-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rust LLVM IR finding","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"F-RUST-IR-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3b-rust-compiler-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rust assembly finding","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"F-RUST-ASM-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3b-rust-compiler-analyzer","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Translation unit","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TU-{hash}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Orchestrator","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Final finding","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ZA-NNNN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4-report-assembler","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"Every finding JSON object includes ","type":"text"},{"text":"related_objects","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"related_findings","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"evidence_files","type":"text","marks":[{"type":"code_inline"}]},{"text":" fields for cross-referencing between agents.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Detection Strategy","type":"text"}]},{"type":"paragraph","content":[{"text":"Analysis runs in two phases. For complete step-by-step guidance, see ","type":"text"},{"text":"{baseDir}/references/detection-strategy.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Steps","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Findings produced","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required tooling","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 1 (Source)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1–6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MISSING_SOURCE_ZEROIZE","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"PARTIAL_WIPE","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"NOT_ON_ALL_PATHS","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"SECRET_COPY","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"INSECURE_HEAP_ALLOC","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source + compile DB","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 2 (Compiler)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"7–12","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OPTIMIZED_AWAY_ZEROIZE","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"STACK_RETENTION","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text","marks":[{"type":"em"}]},{"text":"REGISTER_SPILL","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":", ","type":"text"},{"text":"LOOP_UNROLLED_INCOMPLETE","type":"text","marks":[{"type":"code_inline"}]},{"text":"†, ","type":"text"},{"text":"MISSING_ON_ERROR_PATH","type":"text","marks":[{"type":"code_inline"}]},{"text":"‡, ","type":"text"},{"text":"NOT_DOMINATING_EXITS","type":"text","marks":[{"type":"code_inline"}]},{"text":"‡","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"clang","type":"text","marks":[{"type":"code_inline"}]},{"text":", IR/ASM tools","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"* requires ","type":"text"},{"text":"enable_asm=true","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default) † requires ","type":"text"},{"text":"enable_semantic_ir=true","type":"text","marks":[{"type":"code_inline"}]},{"text":" ‡ requires ","type":"text"},{"text":"enable_cfg=true","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Format","type":"text"}]},{"type":"paragraph","content":[{"text":"Each run produces two outputs:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"final-report.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Comprehensive markdown report (primary human-readable output)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"findings.json","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Structured JSON matching ","type":"text"},{"text":"{baseDir}/schemas/output.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" (for machine consumption and downstream tools)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Markdown Report Structure","type":"text"}]},{"type":"paragraph","content":[{"text":"The markdown report (","type":"text"},{"text":"final-report.md","type":"text","marks":[{"type":"code_inline"}]},{"text":") contains these sections:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Header","type":"text","marks":[{"type":"strong"}]},{"text":": Run metadata (run_id, timestamp, repo, compile_db, config summary)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Executive Summary","type":"text","marks":[{"type":"strong"}]},{"text":": Finding counts by severity, confidence, and category","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sensitive Objects Inventory","type":"text","marks":[{"type":"strong"}]},{"text":": Table of all identified objects with IDs, types, locations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Findings","type":"text","marks":[{"type":"strong"}]},{"text":": Grouped by severity then confidence. Each finding includes location, object, all evidence (source/IR/ASM/CFG), compiler evidence details, and recommended fix","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Superseded Findings","type":"text","marks":[{"type":"strong"}]},{"text":": Source findings replaced by CFG-backed findings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Confidence Gate Summary","type":"text","marks":[{"type":"strong"}]},{"text":": Downgrades applied and overrides rejected","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Analysis Coverage","type":"text","marks":[{"type":"strong"}]},{"text":": TUs analyzed, agent success/failure, features enabled","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Appendix: Evidence Files","type":"text","marks":[{"type":"strong"}]},{"text":": Mapping of finding IDs to evidence file paths","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Structured JSON","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"findings.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" file follows the schema in ","type":"text"},{"text":"{baseDir}/schemas/output.json","type":"text","marks":[{"type":"code_inline"}]},{"text":". Each ","type":"text"},{"text":"Finding","type":"text","marks":[{"type":"code_inline"}]},{"text":" object:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"id\": \"ZA-0001\",\n \"category\": \"OPTIMIZED_AWAY_ZEROIZE\",\n \"severity\": \"high\",\n \"confidence\": \"confirmed\",\n \"language\": \"c\",\n \"file\": \"src/crypto.c\",\n \"line\": 42,\n \"symbol\": \"key_buf\",\n \"evidence\": \"store volatile i8 0 count: O0=32, O2=0 — wipe eliminated by DSE\",\n \"compiler_evidence\": {\n \"opt_levels\": [\"O0\", \"O2\"],\n \"o0\": \"32 volatile stores targeting key_buf\",\n \"o2\": \"0 volatile stores (all eliminated)\",\n \"diff_summary\": \"All volatile wipe stores removed at O2 — classic DSE pattern\"\n },\n \"suggested_fix\": \"Replace memset with explicit_bzero or add compiler_fence(SeqCst) after the wipe\",\n \"poc\": {\n \"file\": \"generated_pocs/ZA-0001.c\",\n \"makefile_target\": \"ZA-0001\",\n \"compile_opt\": \"-O2\",\n \"requires_manual_adjustment\": false,\n \"validated\": true,\n \"validation_result\": \"exploitable\"\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"{baseDir}/schemas/output.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full schema and enum values.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Confidence Gating","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Evidence thresholds","type":"text"}]},{"type":"paragraph","content":[{"text":"A finding requires at least ","type":"text"},{"text":"2 independent signals","type":"text","marks":[{"type":"strong"}]},{"text":" to be marked ","type":"text"},{"text":"confirmed","type":"text","marks":[{"type":"code_inline"}]},{"text":". With 1 signal, mark ","type":"text"},{"text":"likely","type":"text","marks":[{"type":"code_inline"}]},{"text":". With 0 strong signals (name-pattern match only), mark ","type":"text"},{"text":"needs_review","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Signals include: name pattern match, type hint match, explicit annotation, IR evidence, ASM evidence, MCP cross-reference, CFG evidence, PoC validation.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"PoC validation as evidence signal","type":"text"}]},{"type":"paragraph","content":[{"text":"Every finding is validated against a bespoke PoC. After compilation and execution, each PoC is also verified to ensure it actually tests the claimed vulnerability. The combined result is an evidence signal:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PoC Result","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verified","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Impact","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Exit 0 (exploitable)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Strong signal — can upgrade ","type":"text"},{"text":"likely","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"confirmed","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Exit 1 (not exploitable)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Downgrade severity to ","type":"text"},{"text":"low","type":"text","marks":[{"type":"code_inline"}]},{"text":" (informational); retain in report","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Exit 0 or 1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No (user accepted)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Weaker signal — note verification failure in evidence","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Exit 0 or 1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No (user rejected)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No confidence change; annotate as ","type":"text"},{"text":"rejected","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Compile failure / no PoC","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No confidence change; annotate in evidence","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"MCP unavailability downgrade","type":"text"}]},{"type":"paragraph","content":[{"text":"When ","type":"text"},{"text":"mcp_mode=prefer","type":"text","marks":[{"type":"code_inline"}]},{"text":" and MCP is unavailable, downgrade the following unless independent IR/CFG/ASM evidence is strong (2+ signals without MCP):","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Finding","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Downgraded confidence","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SECRET_COPY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"needs_review","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MISSING_ON_ERROR_PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"needs_review","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"NOT_DOMINATING_EXITS","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"needs_review","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Hard evidence requirements (non-negotiable)","type":"text"}]},{"type":"paragraph","content":[{"text":"These findings are ","type":"text"},{"text":"never valid without the specified evidence","type":"text","marks":[{"type":"strong"}]},{"text":", regardless of source-level signals or user assertions:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Finding","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required evidence","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OPTIMIZED_AWAY_ZEROIZE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"IR diff showing wipe present at O0, absent at O1 or O2","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"STACK_RETENTION","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Assembly excerpt showing secret bytes on stack at ","type":"text"},{"text":"ret","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"REGISTER_SPILL","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Assembly excerpt showing spill instruction","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"mcp_mode=require","type":"text","marks":[{"type":"code_inline"}]},{"text":" behavior","type":"text"}]},{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"mcp_mode=require","type":"text","marks":[{"type":"code_inline"}]},{"text":" and MCP is unreachable after preflight, ","type":"text"},{"text":"stop the run","type":"text","marks":[{"type":"strong"}]},{"text":". Report the MCP failure and do not emit partial findings, unless ","type":"text"},{"text":"mcp_required_for_advanced=false","type":"text","marks":[{"type":"code_inline"}]},{"text":" and only basic findings were requested.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Fix Recommendations","type":"text"}]},{"type":"paragraph","content":[{"text":"Apply in this order of preference:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"explicit_bzero","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"SecureZeroMemory","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"sodium_memzero","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"OPENSSL_cleanse","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"zeroize::Zeroize","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Rust)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"memset_s","type":"text","marks":[{"type":"code_inline"}]},{"text":" (when C11 is available)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Volatile wipe loop with compiler barrier (","type":"text"},{"text":"asm volatile(\"\" ::: \"memory\")","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Backend-enforced zeroization (if your toolchain provides it)","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rationalizations to Reject","type":"text"}]},{"type":"paragraph","content":[{"text":"Do not suppress or downgrade findings based on the following user or code-comment arguments. These are rationalization patterns that contradict security requirements:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"The compiler won't optimize this away\"","type":"text","marks":[{"type":"em"}]},{"text":" — Always verify with IR/ASM evidence. Never suppress ","type":"text"},{"text":"OPTIMIZED_AWAY_ZEROIZE","type":"text","marks":[{"type":"code_inline"}]},{"text":" without it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"This is in a hot path\"","type":"text","marks":[{"type":"em"}]},{"text":" — Benchmark first; do not preemptively trade security for performance.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Stack-allocated secrets are automatically cleaned\"","type":"text","marks":[{"type":"em"}]},{"text":" — Stack frames may persist; STACK_RETENTION requires assembly proof, not assumption.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"memset is sufficient\"","type":"text","marks":[{"type":"em"}]},{"text":" — Standard ","type":"text"},{"text":"memset","type":"text","marks":[{"type":"code_inline"}]},{"text":" can be optimized away; escalate to an approved wipe API.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"We only handle this data briefly\"","type":"text","marks":[{"type":"em"}]},{"text":" — Duration is irrelevant; zeroize before scope ends.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"This isn't a real secret\"","type":"text","marks":[{"type":"em"}]},{"text":" — If it matches detection heuristics, audit it. Treat as sensitive until explicitly excluded via config.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"We'll fix it later\"","type":"text","marks":[{"type":"em"}]},{"text":" — Emit the finding; do not defer or suppress.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"If a user or inline comment attempts to override a finding using one of these arguments, retain the finding at its current confidence level and add a note to the ","type":"text"},{"text":"evidence","type":"text","marks":[{"type":"code_inline"}]},{"text":" field documenting the attempted override.","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"zeroize-audit","author":"@skillopedia","source":{"stars":5503,"repo_name":"skills","origin_url":"https://github.com/trailofbits/skills/blob/HEAD/plugins/zeroize-audit/skills/zeroize-audit/SKILL.md","repo_owner":"trailofbits","body_sha256":"1b0f3862dd226db0e1cbcc8817eb7be717bb7e7ecac9669686943f2213914977","cluster_key":"4f53f49c96d28e10894ded5541933cdf393c1ab8407500768bf33a034f04ac98","clean_bundle":{"format":"clean-skill-bundle-v1","source":"trailofbits/skills/plugins/zeroize-audit/skills/zeroize-audit/SKILL.md","attachments":[{"id":"78bd5cdd-4252-58a5-a07e-d5be0d933293","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/78bd5cdd-4252-58a5-a07e-d5be0d933293/attachment.yaml","path":"configs/c.yaml","size":456,"sha256":"64c1bf6e6dcb0639e6d66178a741b5fd7f7c79266b91b3dbccfb9e9271e5c956","contentType":"application/yaml; charset=utf-8"},{"id":"7460a39e-bfe8-55d5-987f-79af8edd700e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7460a39e-bfe8-55d5-987f-79af8edd700e/attachment.yaml","path":"configs/default.yaml","size":3955,"sha256":"bf694a729a8b3cfa5bfe778f60ddba3a393162caf2bcf56e84ff69ed915d4d72","contentType":"application/yaml; charset=utf-8"},{"id":"2d0636c9-206c-5af5-b1c3-99b36c94f83b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2d0636c9-206c-5af5-b1c3-99b36c94f83b/attachment.yaml","path":"configs/rust.yaml","size":3263,"sha256":"3d52fe38092491aea4f3e4745393a61dd08a129c76a34a295dee8c8c614ab1f3","contentType":"application/yaml; charset=utf-8"},{"id":"73ab8687-d069-5219-b7b5-01f78c61cca1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/73ab8687-d069-5219-b7b5-01f78c61cca1/attachment.md","path":"prompts/report_template.md","size":6681,"sha256":"05ee3866e6ee8eaba68717ec8b4b9725ac2853e97db01deed609c4a7bce3587f","contentType":"text/markdown; charset=utf-8"},{"id":"77dda844-e92f-5c59-99fa-62653b82c1ba","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/77dda844-e92f-5c59-99fa-62653b82c1ba/attachment.md","path":"prompts/system.md","size":8128,"sha256":"4e70fd23162925e942ec16009de4bb417cc6edb5a620f53302913de4156954a3","contentType":"text/markdown; charset=utf-8"},{"id":"00077974-3040-520d-a396-24d8fc8a2a46","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/00077974-3040-520d-a396-24d8fc8a2a46/attachment.md","path":"prompts/task.md","size":4428,"sha256":"dd739ef5210841bc3bceacd32bba523dc80ceedf2a534b25b9aba6226e62d4b1","contentType":"text/markdown; charset=utf-8"},{"id":"2156ff87-2acd-53b9-9657-58ab21540d2b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2156ff87-2acd-53b9-9657-58ab21540d2b/attachment.md","path":"references/compile-commands.md","size":10160,"sha256":"ee40f46f9f59c77d9e71b5c7244e9a21f2db07ca29817807f65da9f3e3be877c","contentType":"text/markdown; charset=utf-8"},{"id":"f36657ac-bdc7-5493-b4da-942cfdbaaff6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f36657ac-bdc7-5493-b4da-942cfdbaaff6/attachment.md","path":"references/detection-strategy.md","size":10187,"sha256":"acdf4242f8719ec7c772482f382321d11c6fc0724681cf72f7701cddcdca323e","contentType":"text/markdown; charset=utf-8"},{"id":"46fe3cde-ff53-5f18-83b2-6133d15d0494","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/46fe3cde-ff53-5f18-83b2-6133d15d0494/attachment.md","path":"references/ir-analysis.md","size":11195,"sha256":"473b57e4977e0d04bed503c79c1baa1d9eccee3ba7517855faf442cfeb50e68e","contentType":"text/markdown; charset=utf-8"},{"id":"45037c15-0284-5bee-8f5f-7e5c53f5fc43","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/45037c15-0284-5bee-8f5f-7e5c53f5fc43/attachment.md","path":"references/mcp-analysis.md","size":9257,"sha256":"704a9c6bea280d329a916f437afdd87d36251162e0078b4aecf063acce9de057","contentType":"text/markdown; charset=utf-8"},{"id":"b8d5017a-d2dd-5ecb-a24e-128d016486e9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b8d5017a-d2dd-5ecb-a24e-128d016486e9/attachment.md","path":"references/poc-generation.md","size":21651,"sha256":"61fb008ce7bab1a19b56b25a934d2586d3a6e43bfdbb0327f33f36b9505f5f5c","contentType":"text/markdown; charset=utf-8"},{"id":"49ebeca8-cc7d-5caf-af87-9b53a929842e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/49ebeca8-cc7d-5caf-af87-9b53a929842e/attachment.md","path":"references/rust-zeroization-patterns.md","size":35542,"sha256":"5a86f5accad96827da494e08fef557de845df643c02685e26530403e43e707f2","contentType":"text/markdown; charset=utf-8"},{"id":"f462cfea-0d47-5b79-981d-4903c5be186e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f462cfea-0d47-5b79-981d-4903c5be186e/attachment.json","path":"schemas/input.json","size":3174,"sha256":"0a93fd1ec9e9a4f85355bf40600fd29c76c3f98089d9b6ac76795d1aa0a02421","contentType":"application/json; charset=utf-8"},{"id":"4aab98d1-9907-5a9f-a405-911f400d909a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4aab98d1-9907-5a9f-a405-911f400d909a/attachment.json","path":"schemas/output.json","size":6040,"sha256":"642b4f77d447167d2887d9b5d2ae41d67ac80ba35d7d0be26498adcfbdc02e53","contentType":"application/json; charset=utf-8"},{"id":"1846bc30-af55-57fd-ba27-64b5aac256d8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1846bc30-af55-57fd-ba27-64b5aac256d8/attachment.sh","path":"tools/analyze_asm.sh","size":5286,"sha256":"f072d58e5874b28c9b53ed26a2c7118a3cd994cb75504d56c4dc83ff5fd9de43","contentType":"application/x-sh; charset=utf-8"},{"id":"b43acfe0-18c9-572b-b3d7-51c31fed0879","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b43acfe0-18c9-572b-b3d7-51c31fed0879/attachment.py","path":"tools/analyze_cfg.py","size":13056,"sha256":"1a5bede45f21383c3377a5e639fa676bfe204154303b80c98cd59feaebec2c32","contentType":"text/x-python; charset=utf-8"},{"id":"47cd0fe1-983e-5fc7-b941-da660abfc138","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/47cd0fe1-983e-5fc7-b941-da660abfc138/attachment.sh","path":"tools/analyze_heap.sh","size":5580,"sha256":"ad7d83bd10fa9968d76922c63cbd41256fcbb93f18e3d4d6cf8e8310aa8ad5d9","contentType":"application/x-sh; charset=utf-8"},{"id":"76f515bc-46fd-5480-b770-14e96721c18f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/76f515bc-46fd-5480-b770-14e96721c18f/attachment.py","path":"tools/analyze_ir_semantic.py","size":15149,"sha256":"202a002ce09f5d0e1cccbf3a7791231a61795b6e2f4dcf2d384151278a0d788a","contentType":"text/x-python; charset=utf-8"},{"id":"f728dcfe-c5f0-5854-b088-2e27fc955df8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f728dcfe-c5f0-5854-b088-2e27fc955df8/attachment.sh","path":"tools/diff_ir.sh","size":4111,"sha256":"2fa82d280acbab6aeffad7cd11fe88dd5388c2ad9df50b4a22cc0529e1687ec7","contentType":"application/x-sh; charset=utf-8"},{"id":"eaf36250-5842-522a-addf-83eb1cf723e3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eaf36250-5842-522a-addf-83eb1cf723e3/attachment.sh","path":"tools/diff_rust_mir.sh","size":6397,"sha256":"2d383e50a3ccca17f5a8e5af26f08241cd5811e330f9e5d2ce9e5a249778cbdd","contentType":"application/x-sh; charset=utf-8"},{"id":"8ff8b749-4f4f-5153-9915-3021f85a6a36","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8ff8b749-4f4f-5153-9915-3021f85a6a36/attachment.sh","path":"tools/emit_asm.sh","size":1026,"sha256":"af4db5b52071eb52f1f5579438c199142453e9fb6e6dd775c31a3edd9ea5f34e","contentType":"application/x-sh; charset=utf-8"},{"id":"13affc5f-f63d-5f94-ad1f-376a523abbad","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/13affc5f-f63d-5f94-ad1f-376a523abbad/attachment.sh","path":"tools/emit_ir.sh","size":1340,"sha256":"82a3c91291ff5bc4b0ca78c3b1587aae88c1752774e0b6837c455dec5475ebf4","contentType":"application/x-sh; charset=utf-8"},{"id":"f4e1d4fc-2dcb-5908-9832-ac8922caffa9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f4e1d4fc-2dcb-5908-9832-ac8922caffa9/attachment.sh","path":"tools/emit_rust_asm.sh","size":4337,"sha256":"d23f859b9f4871a625d2b1fffd8857625c5b49a1a8b5e8f4878d0e800fb05847","contentType":"application/x-sh; charset=utf-8"},{"id":"0c173895-fc19-5cb6-91c4-b361840c3bd5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0c173895-fc19-5cb6-91c4-b361840c3bd5/attachment.sh","path":"tools/emit_rust_ir.sh","size":3411,"sha256":"3117475bba0a1fe350a938403597dec7a86d032a6d51494d708ae0037747dd17","contentType":"application/x-sh; charset=utf-8"},{"id":"c4c26a42-d949-5eec-b767-2782a1127d74","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c4c26a42-d949-5eec-b767-2782a1127d74/attachment.sh","path":"tools/emit_rust_mir.sh","size":3536,"sha256":"95e3588f4484c6a872363a44e16587a51f5ba47cb05e6047edd7c598c6aa192d","contentType":"application/x-sh; charset=utf-8"},{"id":"ed6d324e-90b8-58c5-9db5-e9bd8d950ed0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ed6d324e-90b8-58c5-9db5-e9bd8d950ed0/attachment.py","path":"tools/extract_compile_flags.py","size":8782,"sha256":"deb84d34ad07d77d6d16e63eee4ae6bfe492fa8a1893ca9d619dffd7dfee2c65","contentType":"text/x-python; charset=utf-8"},{"id":"9fc38712-9bd1-55e8-9573-6edb8a1a1bff","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9fc38712-9bd1-55e8-9573-6edb8a1a1bff/attachment.py","path":"tools/generate_poc.py","size":48412,"sha256":"d3b44351a526773a513034bad7f838dc61ceb4a1a1e77b737d72c15b7c820005","contentType":"text/x-python; charset=utf-8"},{"id":"5f822f8c-0e3b-5de6-b69e-e5de55a95a98","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5f822f8c-0e3b-5de6-b69e-e5de55a95a98/attachment.py","path":"tools/mcp/apply_confidence_gates.py","size":3374,"sha256":"e4b9f1e2df523614ca61c0c1a89cafad05f487056cfe40aafa6fef4546d5e2f8","contentType":"text/x-python; charset=utf-8"},{"id":"584e74be-4005-5a52-bd25-a010a71aeaf9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/584e74be-4005-5a52-bd25-a010a71aeaf9/attachment.sh","path":"tools/mcp/check_mcp.sh","size":1251,"sha256":"68c9291036b42551f926bb2bd0adeeb11e920232f2d09d4a22712575f1a9a3ae","contentType":"application/x-sh; charset=utf-8"},{"id":"ae4e3bdf-5fcf-53b1-a51c-c09718e36624","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ae4e3bdf-5fcf-53b1-a51c-c09718e36624/attachment.py","path":"tools/mcp/normalize_mcp_evidence.py","size":3906,"sha256":"5c7132a27f7ca1ac88728115273a78d6b11a8f87050d8262fa1c9dd47483b7eb","contentType":"text/x-python; charset=utf-8"},{"id":"deecdeb3-6f45-5ef0-8735-c6428504fbd2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/deecdeb3-6f45-5ef0-8735-c6428504fbd2/attachment.py","path":"tools/scripts/check_llvm_patterns.py","size":17576,"sha256":"0a17e661233fa48feab2b84fa08b685632bef51c8a479c0504e2de6369e73b5c","contentType":"text/x-python; charset=utf-8"},{"id":"cb00093b-ce8e-556f-a9dc-f00f44c94d94","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cb00093b-ce8e-556f-a9dc-f00f44c94d94/attachment.py","path":"tools/scripts/check_mir_patterns.py","size":19399,"sha256":"6591eac1ea39b5e2e4cab93fcc01a3f9db6ea421676583bb31e6677140ff6cef","contentType":"text/x-python; charset=utf-8"},{"id":"002419f4-6efd-5cdf-a153-a243e7362d2d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/002419f4-6efd-5cdf-a153-a243e7362d2d/attachment.py","path":"tools/scripts/check_rust_asm.py","size":16243,"sha256":"3c82bdb8db9c4393949cc437ff3ade8da3a610c27ebbfb54501132f58ad6d675","contentType":"text/x-python; charset=utf-8"},{"id":"c54eaa75-e4cd-5a40-9817-b0d923211c2f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c54eaa75-e4cd-5a40-9817-b0d923211c2f/attachment.py","path":"tools/scripts/check_rust_asm_aarch64.py","size":10246,"sha256":"0d63782b2b2f1a0e632451823d6001e4c8b240844a23c45c1e646602698e8db4","contentType":"text/x-python; charset=utf-8"},{"id":"91dca4d8-413a-5299-beb7-21c1a494954a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/91dca4d8-413a-5299-beb7-21c1a494954a/attachment.py","path":"tools/scripts/check_rust_asm_x86.py","size":9587,"sha256":"6edcdc741d1f202566c5914b5d577c879d11ba61a841ba42398e05579b919cea","contentType":"text/x-python; charset=utf-8"},{"id":"3ab0e627-481d-5acf-953e-3c6b582677f5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3ab0e627-481d-5acf-953e-3c6b582677f5/attachment.py","path":"tools/scripts/find_dangerous_apis.py","size":14132,"sha256":"5a7b873e17a64bdbe0743a0301f1abd0aba4bbe79e552eccdf7dc39e80563469","contentType":"text/x-python; charset=utf-8"},{"id":"a70effc1-3abc-586a-a391-f5384527a38b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a70effc1-3abc-586a-a391-f5384527a38b/attachment.py","path":"tools/scripts/semantic_audit.py","size":33243,"sha256":"b84e3e885e07f88dce17da03d067c1bf7a7ade0305209bdc2e9be28ecad757d9","contentType":"text/x-python; charset=utf-8"},{"id":"68386df0-58de-5b1c-947d-9d9ebc3216fb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/68386df0-58de-5b1c-947d-9d9ebc3216fb/attachment.sh","path":"tools/track_dataflow.sh","size":5209,"sha256":"c60641893a664589d43c837b2ea4c7999ca51dcf61896455e97df4af0a9c12d0","contentType":"application/x-sh; charset=utf-8"},{"id":"2289ebf2-04e9-5597-b081-d60ffab240a3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2289ebf2-04e9-5597-b081-d60ffab240a3/attachment.sh","path":"tools/validate_rust_toolchain.sh","size":8434,"sha256":"4f294646864874bb93ef0aaceecfcaa3f81743278ffe9c48540f53a70004f49f","contentType":"application/x-sh; charset=utf-8"},{"id":"63fe4551-b4fb-527a-9971-5e3d1e3470ec","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/63fe4551-b4fb-527a-9971-5e3d1e3470ec/attachment.md","path":"workflows/phase-0-preflight.md","size":7476,"sha256":"cdf85ce427c13ae11cd3d35460818cd4e834eee13a1a2f45861a6a7d1e93002b","contentType":"text/markdown; charset=utf-8"},{"id":"2c054576-98cb-5260-93bf-0db2e13d751c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2c054576-98cb-5260-93bf-0db2e13d751c/attachment.md","path":"workflows/phase-1-source-analysis.md","size":4814,"sha256":"23c0b46eb0556d680762793a756b49ad025ac559a7d8f51b6f2a2a2f25e45a98","contentType":"text/markdown; charset=utf-8"},{"id":"bba5f849-ed3c-596a-9e2f-21d7b4ae4a84","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bba5f849-ed3c-596a-9e2f-21d7b4ae4a84/attachment.md","path":"workflows/phase-2-compiler-analysis.md","size":5441,"sha256":"9ac1b095f7966fb985279c963be27fa7c43031c1348893bc91968fe120f24731","contentType":"text/markdown; charset=utf-8"},{"id":"612b4337-b120-55f8-8a2b-8786a089c8ad","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/612b4337-b120-55f8-8a2b-8786a089c8ad/attachment.md","path":"workflows/phase-3-interim-report.md","size":1054,"sha256":"f898af293481782908c4d556ccd511ca190740a72a75258283ff03ee6e1df024","contentType":"text/markdown; charset=utf-8"},{"id":"dff8b8e2-e27a-567f-9742-a2a3e992d563","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dff8b8e2-e27a-567f-9742-a2a3e992d563/attachment.md","path":"workflows/phase-4-poc-generation.md","size":1209,"sha256":"2f7a0daf35c60de699de92c5f6e98555f51302aaa92e80c619ef1dcd86ccff01","contentType":"text/markdown; charset=utf-8"},{"id":"f9f605d8-00ff-52b0-a011-937cde8c9878","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f9f605d8-00ff-52b0-a011-937cde8c9878/attachment.md","path":"workflows/phase-5-poc-validation.md","size":4088,"sha256":"0cd58c2b9b92bab27303af0c78d283d06094b1004ab823641f311c66e3133c5e","contentType":"text/markdown; charset=utf-8"},{"id":"5514330e-81cd-5363-bc93-a538d6deaa82","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5514330e-81cd-5363-bc93-a538d6deaa82/attachment.md","path":"workflows/phase-6-final-report.md","size":1071,"sha256":"9a318c4d9bd2c66e183d2a48a0cdef41e8f7e22d721bdaa4cf766831a7f48af7","contentType":"text/markdown; charset=utf-8"},{"id":"40324b93-0a25-544f-b355-44d2f2ce4461","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/40324b93-0a25-544f-b355-44d2f2ce4461/attachment.md","path":"workflows/phase-7-test-generation.md","size":769,"sha256":"3729993bd51803417139e539f09bb07cbba3b541c44087492c1dd596a84ef2c0","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"4dea51f2cf605a56fab7715c8e9413fe481ec3bc829f831b53b6d64dc6685bca","attachment_count":47,"text_attachments":47,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"plugins/zeroize-audit/skills/zeroize-audit/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"security","import_tag":"clean-skills-v1","description":"Detects missing zeroization of sensitive data in source code and identifies zeroization removed by compiler optimizations, with assembly-level analysis, and control-flow verification. Use for auditing C/C++/Rust code handling secrets, keys, passwords, or other sensitive data.","allowed-tools":"Read Grep Glob Bash Write Task AskUserQuestion mcp__serena__activate_project mcp__serena__find_symbol mcp__serena__find_referencing_symbols mcp__serena__get_symbols_overview"}},"renderedAt":1782986519228}

zeroize-audit — Claude Skill When to Use - Auditing cryptographic implementations (keys, seeds, nonces, secrets) - Reviewing authentication systems (passwords, tokens, session data) - Analyzing code that handles PII or sensitive credentials - Verifying secure cleanup in security-critical codebases - Investigating memory safety of sensitive data handling When NOT to Use - General code review without security focus - Performance optimization (unless related to secure wiping) - Refactoring tasks not related to sensitive data - Code without identifiable secrets or sensitive values --- Purpose Det…