ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_tz_boundary_discard.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Ban silent error discards at TrustZone boot boundaries.
5
6C23 makes an explicit ``(void)`` cast the sanctioned suppression for a
7``[[nodiscard]]`` diagnostic, so ``-Wall -Wextra -Werror`` can never flag
8
9 (void)ra8_cgc_init(); /* discarded right before BLXNS */
10
11even though the callee is ``[[nodiscard]] ra8_err_t``. That exact pattern
12shipped a Secure boot that would BLXNS into the Non-secure image on a dead
13clock tree (tracker issue #191, T2-05), and it recurred after being fixed
14once because per-app boot files are copied from siblings. The compiler is
15structurally unable to police the void-cast; this gate is the backstop.
16
17Rules (first-party C/C++ only):
18
19 A. The TrustZone world-switch API family is must-handle EVERYWHERE: a
20 ``(void)``-cast discard of any ``ra8_tz_secure_boot_*()`` call is
21 banned in every first-party file. A returned error from that family
22 is a root-of-trust DENIAL or a rejected NS vector table -- ignoring
23 it means treating an unauthenticated NS world as live.
24
25 B. Inside a boot translation unit -- any ``.c`` that defines
26 ``SystemInit`` or ``ra8_trustzone_init`` -- a ``(void)``-cast discard
27 of ANY ``ra8_*()`` call is banned. Every fallible step in those TUs
28 gates the S->NS world switch; a tolerated failure must be an explicit
29 ``if`` (halt, or a documented S-side fallback) or
30 ``RA8_ERROR_CHECK`` / ``RA8_ERROR_CHECK_NO_ABORT``, never a silent
31 cast.
32
33Scope:
34 C / C++ sources (.c .h .cpp .hpp) under libs/, src/, tests/, examples/,
35 port/, tools/. Vendored SOUP (libs/third_party/*) and generated font
36 data (libs/ra8_fonts/*) are exempt, as are build trees.
37
38Exemptions:
39 * Comment lines / prose (a match whose cast sits inside a // or /*
40 comment does not count).
41 * Function-pointer discards like ``(void)ra8_trustzone_init;`` (no call
42 parentheses) -- those silence -Wunused, they do not discard an error.
43 * Any line carrying `TZ-DISCARD-OK: <reason>` (reason text required).
44
45Exit 0 when clean, 1 on any discard, 2 when the whole-tree sweep collapses
46below FILE_FLOOR (see the constant).
47"""
48
49from __future__ import annotations
50
51import re
52import sys
53import tempfile
54from pathlib import Path
55
56sys.path.insert(0, str(Path(__file__).resolve().parent))
57
58from lint_targets import is_build_output_path
59
60ROOTS = ("libs", "tests", "examples", "port", "tools", "apps")
61EXTS = (".c", ".h", ".cpp", ".hpp")
62EXEMPT_DIRS = ("/third_party/", "/ra8_fonts/")
63
64# Rule A: the world-switch family, discarded anywhere.
65FAMILY_RE = re.compile(r"\‍(\s*void\s*\‍)\s*(ra8_tz_secure_boot_[a-z0-9_]+)\s*\‍(")
66# Rule B: any ra8_ call, discarded inside a boot TU.
67ANY_RA8_RE = re.compile(r"\‍(\s*void\s*\‍)\s*(ra8_[a-z0-9_]+)\s*\‍(")
68# Boot-TU marker: a definition (or same-file declaration) of one of the two
69# canonical boot entry points every app's boot files provide.
70BOOT_TU_RE = re.compile(
71 r"^\s*void\s+(?:SystemInit|ra8_trustzone_init)\s*\‍(\s*void\s*\‍)", re.MULTILINE
72)
73WAIVER_RE = re.compile(r"TZ-DISCARD-OK:\s*\S")
74
75# A tree this size cannot legitimately collapse to a handful of files. If the
76# whole-tree sweep returns less than this, something broke (a bad cwd -- ROOTS
77# are relative, so this walks nothing when run from elsewhere -- or a renamed
78# root) and reporting "clean" would be a lie: no boot TU would be read, so no
79# discard could ever be reported. Measured 2026-07-28: 2125 first-party C/C++
80# files. Same trip-wire as check_ruff.py.
81FILE_FLOOR = 1700
82
83
84def _is_comment_pos(line: str, pos: int) -> bool:
85 """True if column `pos` sits inside a // or /* comment on this line."""
86 stripped = line.lstrip()
87 if stripped.startswith(("*", "//", "/*")):
88 return True
89 before = line[:pos]
90 if "//" in before:
91 return True
92 # crude single-line /* ... */ check
93 return "/*" in before and "*/" not in before
94
95
96def discover() -> list[str]:
97 """Every boot-boundary source file, for the whole-tree sweep."""
98 out: list[str] = []
99 for root in ROOTS:
100 base = Path(root)
101 if not base.is_dir():
102 continue
103 for ext in EXTS:
104 for f in base.rglob(f"*{ext}"):
105 s = str(f)
106 if is_build_output_path(s) or any(d in s for d in EXEMPT_DIRS):
107 continue
108 out.append(s)
109 return out
110
111
112def check_file(path: str) -> list[tuple[int, str, str]]:
113 """Return (line, rule, snippet) findings for one file."""
114 findings: list[tuple[int, str, str]] = []
115 try:
116 text = Path(path).read_text(encoding="utf-8")
117 except (OSError, UnicodeDecodeError):
118 return findings
119 if "(void)" not in text.replace(" ", ""):
120 return findings
121 boot_tu = path.endswith(".c") and bool(BOOT_TU_RE.search(text))
122 rules: list[tuple[re.Pattern[str], str]] = [(FAMILY_RE, "A")]
123 if boot_tu:
124 rules.append((ANY_RA8_RE, "B"))
125 lines = text.splitlines()
126 for i, raw in enumerate(lines, 1):
127 # A discard split across lines: `(void)` alone, call on the next line.
128 line = raw
129 if re.search(r"\‍(\s*void\s*\‍)\s*$", raw) and i < len(lines):
130 line = raw + lines[i]
131 if WAIVER_RE.search(line):
132 continue
133 seen: set[tuple[int, int]] = set()
134 for rule_re, rule in rules:
135 for m in rule_re.finditer(line):
136 span = (m.start(), m.end())
137 if span in seen or _is_comment_pos(line, m.start()):
138 continue
139 seen.add(span)
140 findings.append((i, rule, raw.strip()[:100]))
141 return findings
142
143
144def selftest() -> int:
145 """Prove both boundary discard rules fire and handled/waived calls stay quiet."""
146 with tempfile.TemporaryDirectory(prefix="tz-discard-selftest-") as raw:
147 root = Path(raw)
148 family_bad = root / "ordinary.c"
149 boot_bad = root / "boot.c"
150 good = root / "good.c"
151 family_bad.write_text(
152 "void f(void) { (void)ra8_tz_secure_boot_verify(); }\n", encoding="ascii"
153 )
154 boot_bad.write_text("void SystemInit(void) { (void)ra8_cgc_init(); }\n", encoding="ascii")
155 good.write_text(
156 "void SystemInit(void) { if (ra8_cgc_init() != k_ra8_ok) { halt(); } }\n"
157 "void f(void) { (void)ra8_tz_secure_boot_verify(); } "
158 "/* TZ-DISCARD-OK: synthetic documented fallback */\n",
159 encoding="ascii",
160 )
161 bad_findings = check_file(str(family_bad)) + check_file(str(boot_bad))
162 good_findings = check_file(str(good))
163 cases = (
164 (
165 {rule for _line, rule, _snippet in bad_findings} == {"A", "B"},
166 "world-switch and boot-translation-unit discards both fire",
167 ),
168 (not good_findings, "handled results and exact reasoned waiver stay quiet"),
169 )
170 failed = [label for passed, label in cases if not passed]
171 for passed, label in cases:
172 print(f" [{'ok' if passed else 'FAIL'}] {label}")
173 if failed:
174 print(f"check_tz_boundary_discard.py --selftest: {len(failed)} failure(s)", file=sys.stderr)
175 return 1
176 print("check_tz_boundary_discard.py --selftest: all cases pass (both directions).")
177 return 0
178
179
180def main() -> int:
181 """Fail when a TrustZone boot-boundary call discards its ra8_err_t.
182
183 These call sites are the worst place in the tree to drop a status: a
184 failed SAU or MPU configuration returns an error and then, ignored, hands
185 control to the non-secure world with the boundary not actually in force.
186 The build links, the board boots, and the isolation is simply absent.
187
188 FILE_FLOOR is enforced on the whole-tree sweep only, and exits 2 below it.
189 An explicit argv file list is a deliberately narrowed scope -- not a
190 collapsed one -- so it is exempt; the sweep is where a broken enumeration
191 would silently report the tree clean.
192
193 Returns 1 listing each discard, 0 when clean, 2 when the whole-tree sweep
194 enumerated too few files to trust.
195 """
196 raw_args = sys.argv[1:]
197 if raw_args == ["--selftest"]:
198 return selftest()
199 if any(arg.startswith("-") for arg in raw_args):
200 print("usage: check_tz_boundary_discard.py [--selftest] [file ...]", file=sys.stderr)
201 return 2
202 args = raw_args
203 whole_tree = not args
204 files = args or discover()
205 if whole_tree and len(files) < FILE_FLOOR:
206 print(
207 f"check_tz_boundary_discard.py: FATAL -- only {len(files)} first-party source "
208 f"file(s) in scope, floor is {FILE_FLOOR}. A collapsed sweep reports a "
209 "clean tree because it scanned nothing.",
210 file=sys.stderr,
211 )
212 return 2
213 total = 0
214 for path in sorted(set(files)):
215 if (
216 not path.endswith(EXTS)
217 or is_build_output_path(path)
218 or any(d in path for d in EXEMPT_DIRS)
219 ):
220 continue
221 for ln, rule, snippet in check_file(path):
222 what = (
223 "world-switch result discarded" if rule == "A" else "boot-TU ra8_* result discarded"
224 )
225 print(
226 f"{path}:{ln}: [rule {rule}] {what} -- handle the ra8_err_t "
227 f"(halt or a documented fallback; RA8_ERROR_CHECK[_NO_ABORT]); "
228 f"never (void)-cast it at a TrustZone boot boundary; {snippet}"
229 )
230 total += 1
231 if total:
232 print(
233 f"\ncheck_tz_boundary_discard: {total} violation(s). A (void)-cast "
234 f"silences [[nodiscard]] by ISO C23 rule, so -Werror cannot catch "
235 f"these; check the result and fail safe instead (add "
236 f"`TZ-DISCARD-OK: <reason>` only for a justified exception)."
237 )
238 return 1
239 print(
240 "check_tz_boundary_discard: clean -- no silent ra8_err_t discards at "
241 "TrustZone boot boundaries."
242 )
243 return 0
244
245
246if __name__ == "__main__":
247 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298