ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_sg_offsets.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"""Verify the NSC Secure-Gateway veneer slot offsets in a linked ELF.
5
6The tz_nsc_cgc_usb Non-Secure image reaches each NSC CGC veneer by NAME, not by
7address: ``ns_main.c`` declares the bare ``ra8_nsc_cgc_*`` prototypes and calls
8them, and the CMSE import library the Secure link emits (``--cmse-implib
9--out-implib``, on this image's link line) binds those names to the
10Secure-Gateway stub addresses. That import library is derived FROM the Secure
11ELF, so the byte offset of each veneer inside the ``.gnu.sgstubs`` region IS the
12ABI contract between the two worlds: reorder the stubs and every bound
13NS->Secure call lands on a different entry point, with no diagnostic.
14
15This post-build check reads the FINAL Secure symbol table (where ``nm`` reports
16the real SG-stub addresses) and fails the build if any veneer has drifted from
17its pinned offset in ``EXPECTED_OFFSETS`` below. A drift means the Secure ELF no
18longer matches the offsets this table records -- and therefore no longer matches
19the import library the NS image was bound against; re-derive the offsets from
20the link and update the table. (There is no ``k_sg_off_*`` enum to keep in step
21any more: the NS side gave up hard-coded offsets for the import library.)
22
23The veneer set is REQUIRED, not optional, once the ELF has an NSC region. Both
24callers -- the ``tz_nsc_cgc_usb.elf`` POST_BUILD command and the ``sg-offsets``
25gate -- pick that ELF precisely BECAUSE it binds all three ``ra8_nsc_cgc_*``
26veneers (its CMakeLists passes ``NSC_SRCS ra8_nsc_cgc.c``, whose three
27``RA8_NSC_VENEER`` definitions are the whole point of the app). So an absent
28veneer there is a broken secure gateway, not a build configuration, and the
29old ``any(...)`` guard turned exactly that defect into ``skipped.`` + exit 0.
30Only an ELF with no ``g_ra8_ls_sgstubs_start`` at all -- a link whose script
31never placed ``.gnu.sgstubs`` -- is still skipped.
32
33Usage:
34 check_sg_offsets.py <elf> [--nm <nm-binary>]
35
36Exit codes:
37 0 -- offsets match, or the ELF has no NSC region at all
38 1 -- a veneer slot drifted, or a required veneer is missing from the link
39 2 -- usage / tool error, or a symbol table too small to trust (SYMBOL_FLOOR)
40"""
41
42import argparse
43import contextlib
44import shutil
45import subprocess
46import sys
47
48# Expected byte offset of each SG veneer from g_ra8_ls_sgstubs_start. ld emits
49# the 8-byte stubs in ascending symbol-name order, so this is deterministic.
50EXPECTED_OFFSETS = {
51 "ra8_nsc_cgc_get_clock_hz": 0,
52 "ra8_nsc_cgc_usbfs_clock_enable": 8,
53 "ra8_nsc_cgc_pll2_enable": 16,
54}
55BASE_SYMBOL = "g_ra8_ls_sgstubs_start"
56THUMB_MASK = 0xFFFFFFFE
57
58# nm output has 3 fields: address, type, name.
59NM_FIELD_COUNT = 3
60
61# A linked firmware image cannot legitimately define a handful of symbols. If
62# the parse returns less than this, something broke (the wrong file, a stripped
63# ELF, an nm that printed a format this parser does not recognise) and every
64# later lookup would miss -- which reads as "no SG veneers present" and exits 0
65# having verified nothing. Measured 2026-07-28: 187 defined symbols in
66# tz_nsc_cgc_usb.elf. Same trip-wire as check_ruff.py.
67SYMBOL_FLOOR = 140
68
69
70def read_symbols(elf: str, nm: str) -> dict[str, int]:
71 """Return {symbol: address} for every defined symbol in ``elf``."""
72 out = subprocess.run( # noqa: S603 # trusted: fixed arm-none-eabi-nm argv
73 [nm, elf], capture_output=True, text=True, check=True
74 ).stdout
75 syms: dict[str, int] = {}
76 for line in out.splitlines():
77 parts = line.split()
78 if len(parts) == NM_FIELD_COUNT and parts[1] in ("T", "t", "R", "r", "D", "d", "B", "b"):
79 with contextlib.suppress(ValueError):
80 syms[parts[2]] = int(parts[0], 16)
81 return syms
82
83
84def offset_drift(syms: dict[str, int]) -> list[str]:
85 """Return every missing or displaced required veneer from a symbol table."""
86 base = syms[BASE_SYMBOL]
87 drift: list[str] = []
88 for sym, want in EXPECTED_OFFSETS.items():
89 if sym not in syms:
90 drift.append(f" {sym}: MISSING from the link")
91 continue
92 got = (syms[sym] & THUMB_MASK) - base
93 if got != want:
94 drift.append(f" {sym}: at sgstubs+{got} (expected sgstubs+{want})")
95 return drift
96
97
98def selftest() -> int:
99 """Prove exact Thumb offsets pass while one drift and one absence fire."""
100 base = 0x1000
101 good = {
102 BASE_SYMBOL: base,
103 **{name: base + offset + 1 for name, offset in EXPECTED_OFFSETS.items()},
104 }
105 bad = dict(good)
106 missing = next(iter(EXPECTED_OFFSETS))
107 bad.pop(missing)
108 shifted = next(name for name in EXPECTED_OFFSETS if name != missing)
109 bad[shifted] += 8
110 good_findings = offset_drift(good)
111 bad_findings = offset_drift(bad)
112 expected_bad_findings = 2
113 cases = (
114 (not good_findings, "exact Thumb-normalized veneer offsets stay quiet"),
115 (
116 len(bad_findings) == expected_bad_findings
117 and any("MISSING" in item for item in bad_findings)
118 and any("expected" in item for item in bad_findings),
119 "a missing veneer and a displaced veneer both fire",
120 ),
121 )
122 failed = [label for passed, label in cases if not passed]
123 for passed, label in cases:
124 print(f" [{'ok' if passed else 'FAIL'}] {label}")
125 if failed:
126 print(f"check_sg_offsets.py --selftest: {len(failed)} failure(s)", file=sys.stderr)
127 return 1
128 print("check_sg_offsets.py --selftest: all cases pass (both directions).")
129 return 0
130
131
132def main() -> int:
133 """Verify the secure-gateway veneers sit at their pinned offsets in an ELF.
134
135 The offsets are ABI: non-secure code reaches the secure world by branching
136 into the SG region at a fixed distance from its base, so a veneer moving
137 silently redirects a call to a different entry point. Only the offset from
138 BASE_SYMBOL is compared, never absolute addresses, since the region as a
139 whole is free to relocate between builds.
140
141 Symbol values are masked with THUMB_MASK before subtracting -- every Thumb
142 function symbol carries bit 0 set, and comparing raw values would make
143 every offset off by one.
144
145 Only an ELF with no ``g_ra8_ls_sgstubs_start`` at all is skipped -- a link
146 whose script never placed ``.gnu.sgstubs``, where there is no NSC region to
147 have drifted. Once that base symbol exists, EVERY veneer in
148 EXPECTED_OFFSETS is required: the callers hand this the one ELF that binds
149 all three by construction, so a missing one is a broken secure gateway. The
150 previous ``any(...)`` guard reported that defect as ``skipped.`` and
151 exited 0.
152
153 SYMBOL_FLOOR guards the layer beneath both: a parse that yields almost no
154 symbols makes every lookup miss, which the skip branch would then read as
155 "no veneers present".
156
157 Returns 0 when every offset matches or the ELF has no NSC region, 1 on
158 drift or a missing veneer, 2 when ``nm`` could not be run at all or its
159 output fell below SYMBOL_FLOOR.
160 """
161 ap = argparse.ArgumentParser()
162 ap.add_argument("elf", nargs="?")
163 ap.add_argument("--nm", default="arm-none-eabi-nm")
164 ap.add_argument("--selftest", action="store_true")
165 args = ap.parse_args()
166
167 if args.selftest:
168 if args.elf is not None or args.nm != "arm-none-eabi-nm":
169 ap.error("--selftest does not accept an ELF or --nm")
170 return selftest()
171 if args.elf is None:
172 ap.error("the following arguments are required: elf")
173
174 nm = shutil.which(args.nm) or args.nm
175 try:
176 syms = read_symbols(args.elf, nm)
177 except (OSError, subprocess.CalledProcessError) as exc:
178 print(f"check_sg_offsets: cannot run nm on {args.elf}: {exc}", file=sys.stderr)
179 return 2
180
181 if len(syms) < SYMBOL_FLOOR:
182 print(
183 f"check_sg_offsets: FATAL -- only {len(syms)} defined symbol(s) read from "
184 f"{args.elf}, floor is {SYMBOL_FLOOR}. A collapsed symbol table reports "
185 "'no SG veneers present' because every lookup missed.",
186 file=sys.stderr,
187 )
188 return 2
189
190 if BASE_SYMBOL not in syms:
191 # No .gnu.sgstubs placement at all: there is no NSC region to drift.
192 # A PRESENT base with absent veneers is NOT this case -- it falls
193 # through and every missing veneer is reported below.
194 print("check_sg_offsets: no NSC region in this ELF -- skipped.")
195 return 0
196
197 drift = offset_drift(syms)
198
199 if drift:
200 print("check_sg_offsets: FATAL -- NSC SG-veneer slot drift detected.", file=sys.stderr)
201 print("\n".join(drift), file=sys.stderr)
202 print(
203 "Re-derive the offsets from the Secure link and update EXPECTED_OFFSETS here.",
204 file=sys.stderr,
205 )
206 return 1
207
208 print("check_sg_offsets: NSC SG-veneer slot offsets OK (0/8/16).")
209 return 0
210
211
212if __name__ == "__main__":
213 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298