4"""Reject first-party address constants that point into a Reserved memory window.
8Three times now a driver has named an address that describes hardware which is
9not there, compiled clean, and failed only on silicon:
11 * the `ra8_rsip` crypto family -- invented registers, whole driver rewritten;
12 * `ra8_ptp` -- addressed a reserved window, and its `gptp: clock PASS` was a
13 reserved aperture echoing back a write (#498);
14 * `ra8_wdt_regs.h` -- read OFS0/OFS3 at 0x03001E04 / 0x03001E20, which appear
15 nowhere in either Hardware User's Manual and land in a Reserved area, and
16 dereferenced them at runtime (#545).
18Nothing catches this class. `check_hum_register_map.py` (#540) cross-checks
19register SYMBOLS and struct/window OFFSETS against the manual's tables, but an
20absolute-address enumerator like `k_ra8_wdt_ofs0_addr` is neither, so it falls
21outside that gate's rules. `check_linker_scripts.py` rule LD007 guards the
22phantom data-flash base on the LINKER side only -- the C side was unguarded,
23which is exactly how #545 survived.
27 RA001 an enumerator in a `uintptr_t`-typed enum resolves into a window the
28 HUM memory map marks Reserved.
30The rule is deliberately narrow. `uintptr_t` enums are, by the CLAUDE.md
31constants hierarchy, THE way this tree spells a hardware address ("Use
32`uintptr_t` for any enum whose values are hardware memory-mapped addresses"),
33so the scan surface is precisely the set of things that claim to be addresses.
34A value that is not an address does not belong in one of these enums, and an
35address that lands in a Reserved window is wrong by construction -- reading it
36is undefined: it may fault, or return garbage the caller then trusts.
38WHAT IT DELIBERATELY DOES NOT CHECK
39===================================
40It does not try to validate that a non-reserved address is CORRECT -- that
41needs the manual's register tables, which is #540's job. This gate answers the
42much cheaper question that #540 cannot: "is this address inside a hole?"
45from __future__
import annotations
54from dataclasses
import dataclass
62RESERVED_WINDOWS: list[tuple[int, int, str]] = [
65 (0x03000000, 0x07FFFFFF,
"Reserved area between Extra MRAM and SiP-Flash (HUM Ch 3 map)"),
70 (0x13000000, 0x17FFFFFF,
"Reserved area, non-secure alias of 0x0300_0000 (HUM Ch 3 map)"),
73 (0x27000000, 0x27FFFFFF,
"phantom data-flash -- the RA8 has no data-flash array (#397)"),
78SCAN_ROOTS = (
"libs",
"examples",
"port",
"tools",
"apps",
"tests")
81THIRD_PARTY_PREFIXES = (
"libs/third_party/",
"apps/shared_libs/third_party/")
90ENUM_OPEN = re.compile(
r"\benum\s*:\s*uintptr_t\s*\{", re.MULTILINE)
91ENUMERATOR = re.compile(
r"(\bk_[A-Za-z0-9_]+)\s*=\s*(0[xX][0-9A-Fa-f_]+)")
94@dataclass(frozen=True)
96 """One RA001 hit: an address enumerator inside a Reserved window."""
104 def render(self) -> str:
105 """Format the finding as a single `path:line: [RA001] ...` line."""
107 f
"{self.path}:{self.line}: [RA001] {self.symbol} = 0x{self.value:08X} "
108 f
"lands in a Reserved window -- {self.why}"
112def strip_comments(text: str) -> str:
113 """Blank out block and line comments, preserving line structure.
115 Newlines are kept so reported line numbers stay true to the source. A
116 commented-out address is documentation, not a dereference, so it must not
117 be flagged -- both remediated headers cite the old bad values in prose.
119 text = re.sub(
r"/\*.*?\*/",
lambda m: re.sub(
r"[^\n]",
" ", m.group(0)), text, flags=re.DOTALL)
120 return re.sub(
r"//[^\n]*",
"", text)
123def enum_bodies(code: str) -> list[tuple[int, str]]:
124 """Yield `(offset, body)` for every `enum : uintptr_t { ... }` block.
126 Brace-matched rather than regex-terminated so a nested initialiser cannot
127 truncate the body and silently drop the enumerators after it.
129 out: list[tuple[int, str]] = []
130 for m
in ENUM_OPEN.finditer(code):
134 while i < len(code)
and depth > 0:
140 out.append((start, code[start : i - 1]))
144def reserved_hit(value: int) -> str |
None:
145 """Return the description of the Reserved window containing `value`, if any."""
146 for lo, hi, why
in RESERVED_WINDOWS:
147 if lo <= value <= hi:
152def check_text(path: pathlib.Path, text: str) -> tuple[list[Finding], int]:
153 """Apply RA001 to one file; also return how many enumerators were seen.
155 The enumerator count feeds the vacuity floor, and is produced by the same
156 pass that applies the rule so the two cannot drift.
158 code = strip_comments(text)
159 findings: list[Finding] = []
161 for offset, body
in enum_bodies(code):
162 for m
in ENUMERATOR.finditer(body):
164 value = int(m.group(2).replace(
"_",
""), 16)
165 why = reserved_hit(value)
167 line = code[: offset + m.start()].count(
"\n") + 1
168 findings.append(Finding(path, line, m.group(1), value, why))
169 return findings, seen
172def repo_root() -> pathlib.Path:
173 """Resolve the repository root via git."""
176 [
"git",
"rev-parse",
"--show-toplevel"],
184def tracked_sources(root: pathlib.Path) -> list[pathlib.Path]:
185 """List tracked first-party C sources and headers, excluding SOUP."""
186 out = subprocess.run(
187 [
"git",
"ls-files",
"--", *(f
"{d}/**/*.[ch]" for d
in SCAN_ROOTS)],
193 return [root / p
for p
in out
if not p.startswith(THIRD_PARTY_PREFIXES)]
196def scan(paths: list[pathlib.Path]) -> tuple[list[Finding], int]:
197 """Run RA001 over every path, returning findings and the enumerator count."""
198 findings: list[Finding] = []
202 text = p.read_text(encoding=
"utf-8", errors=
"replace")
205 f, seen = check_text(p, text)
208 return findings, total
211def floor_breached(seen: int) -> bool:
212 """Report and fail when the scan matched too few enumerators to mean anything."""
213 if seen >= ENUMERATOR_FLOOR:
216 f
"ERROR: matched only {seen} uintptr_t enumerator(s), below the floor of "
217 f
"{ENUMERATOR_FLOOR}. Either the tree stopped spelling addresses as "
218 f
"`enum : uintptr_t` (RA001 now enforces nothing) or the scan scope "
219 f
"collapsed. Refusing to report success.",
227_FIXTURE_BAD_OFS0 = 0x03001E04
228_FIXTURE_BAD_DATAFLASH = 0x27000000
229_FIXTURE_QUIET_ENUMERATORS = 3
232typedef enum : uintptr_t {
233 k_ra8_bad_addr = 0x03001E04UL,
237_MUST_FIRE_DATAFLASH =
"""
238typedef enum : uintptr_t {
239 k_ra8_df_addr = 0x27000000UL,
243_MUST_STAY_QUIET =
"""
244/* A commented-out 0x03001E04 is prose, not a dereference. */
245typedef enum : uintptr_t {
246 k_ra8_ofs0_addr = 0x02C9F040UL,
247 k_ra8_ofs3_addr = 0x12C9F4C4UL,
248 k_ra8_wdt_base_addr = 0x40202600UL,
251/* Not a uintptr_t enum: a mask that happens to look like a reserved address. */
252typedef enum : uint32_t {
253 k_ra8_mask_thing = 0x03001E04UL,
258def selftest() -> int:
259 """Assert RA001 fires on a reserved address and stays quiet on a real one.
261 Both directions, driving `check_text` -- the same entry point `scan` uses.
262 A checker that only proves it can fire is indistinguishable from one whose
263 scope has collapsed to nothing.
266 fake = pathlib.Path(
"selftest.h")
268 hits, seen = check_text(fake, _MUST_FIRE)
269 if len(hits) != 1
or hits[0].value != _FIXTURE_BAD_OFS0:
270 print(
"SELFTEST FAIL: RA001 did not fire on the #545 reserved address", file=sys.stderr)
273 print(f
"SELFTEST FAIL: expected 1 enumerator, counted {seen}", file=sys.stderr)
276 hits, _ = check_text(fake, _MUST_FIRE_DATAFLASH)
277 if len(hits) != 1
or hits[0].value != _FIXTURE_BAD_DATAFLASH:
278 print(
"SELFTEST FAIL: RA001 did not fire on phantom data-flash", file=sys.stderr)
281 hits, seen = check_text(fake, _MUST_STAY_QUIET)
284 f
"SELFTEST FAIL: RA001 over-fired on legitimate addresses: "
285 f
"{[h.render() for h in hits]}",
289 if seen != _FIXTURE_QUIET_ENUMERATORS:
291 f
"SELFTEST FAIL: expected {_FIXTURE_QUIET_ENUMERATORS} uintptr_t "
292 f
"enumerators in the quiet fixture, counted {seen} -- the uint32_t "
293 f
"enum must not be scanned",
303 with contextlib.redirect_stderr(sink):
304 floor_bites = floor_breached(0)
306 print(
"SELFTEST FAIL: vacuity floor accepted an empty scan", file=sys.stderr)
310 print(
"check_reserved_addresses selftest: OK (fires, stays quiet, floor bites)")
315 """Scan first-party C for address enumerators inside Reserved windows."""
316 ap = argparse.ArgumentParser(description=__doc__)
317 ap.add_argument(
"--selftest", action=
"store_true", help=
"assert both directions")
318 ap.add_argument(
"--list-files", action=
"store_true", help=
"print the scanned file list")
319 ap.add_argument(
"paths", nargs=
"*", help=
"files to check (default: all tracked)")
320 args = ap.parse_args()
326 paths = [pathlib.Path(p)
for p
in args.paths]
if args.paths
else tracked_sources(root)
333 findings, seen = scan(paths)
335 print(f.render(), file=sys.stderr)
338 if not args.paths
and floor_breached(seen):
343 f
"\n{len(findings)} address constant(s) point into a Reserved window. "
344 f
"An address the manual does not define is not a citation problem -- "
345 f
"reading it is undefined behaviour on silicon.",
352if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.