4"""Both-direction selftests for the freestanding runtime ratchet.
6Exercises map/nm parsing, linker-script checks, baseline ratchet evaluation
7for unknown, indebted, and zero-debt apps, archive allowlist policy, source
8assert scanning, and RA8_FREESTANDING configuration. Imported lazily by
9check_freestanding_runtime.selftest() so the detector module stays small.
12from __future__
import annotations
19import check_freestanding_runtime
as core
22def _selftest_nm_and_map() -> list[str]:
23 """Test nm parsing and map file parsing."""
24 failures: list[str] = []
290200e4d0 T __assert_func
3202003000 T __aeabi_uldivmod
34 syms = core.parse_nm_symbols(mock_nm)
35 if "memset" not in syms
or "malloc" not in syms
or "__aeabi_uldivmod" not in syms:
36 failures.append(
"nm symbol parser missed symbols")
39Archive member included to satisfy reference by file (symbol)
41.../libg_nano.a(libc_a-memcmp.o)
42.../libg_nano.a(libc_a-memset.o)
43.../libnosys.a(close.o)
45Discarded input sections
47 .text 0x00000000 0x60 .../libg_nano.a(libc_a-memcmp.o)
48 .text._sbrk 0x00000000 0x14 CMakeFiles/blink.elf.dir/.../ra8_sbrk_trap.c.obj
54.text 0x02000000 0x1000
55 .text.memset 0x0200109e 0x10 .../libg_nano.a(libc_a-memset.o)
57 .text._sbrk 0x02001234 0x14 CMakeFiles/app.elf.dir/.../ra8_sbrk_trap.c.obj
60 map_res = core.parse_map_file(mock_map)
61 if "libc_a-memcmp.o" in map_res[
"live_archive_members"].get(
"libg_nano.a", set()):
62 failures.append(
"discarded archive member classified as live")
63 if "libc_a-memset.o" not in map_res[
"live_archive_members"].get(
"libg_nano.a", set()):
64 failures.append(
"live archive member not detected")
65 if "close.o" in map_res[
"live_archive_members"].get(
"libnosys.a", set()):
66 failures.append(
"LOAD-only archive member classified as live")
67 if "ra8_sbrk_trap" not in map_res[
"symbol_providers"].get(
"_sbrk",
""):
68 failures.append(f
"_sbrk provider incorrect: {map_res['symbol_providers'].get('_sbrk')}")
72.text 0x02000000 0x1000
73 .text._sbrk 0x02001234 0x20 .../libnosys.a(sbrk.o)
76 bad_map_res = core.parse_map_file(mock_bad_map)
77 if "sbrk.o" not in bad_map_res[
"symbol_providers"].get(
"_sbrk",
""):
78 failures.append(
"libnosys sbrk provider was not flagged")
83def _selftest_linker_scripts() -> list[str]:
84 """Test linker script verification."""
85 failures: list[str] = []
86 clean_ld =
"SECTIONS { .text : { *(.text*) } > SRAM }"
87 bad_ld_end =
"SECTIONS { PROVIDE(end = .); }"
88 bad_ld_heap =
"SECTIONS { .heap : { *(._heap*) } > SRAM }"
90 if core.check_linker_script(clean_ld,
"clean.ld"):
91 failures.append(
"clean linker script flagged")
92 if not core.check_linker_script(bad_ld_end,
"bad_end.ld"):
93 failures.append(
"linker script defining 'end' not flagged")
94 if not core.check_linker_script(bad_ld_heap,
"bad_heap.ld"):
95 failures.append(
"linker script defining '.heap' not flagged")
99def _selftest_new_app_policy(
100 mock_baseline: dict[str, Any], clean_analysis: dict[str, Any]
102 """Test strict symbol and forbidden-archive policy for unknown apps."""
103 failures: list[str] = []
104 if core.evaluate_against_baseline(
"new_app", clean_analysis, mock_baseline):
105 failures.append(
"clean app with libgcc helpers flagged")
107 bad_sym = dict(clean_analysis, live_forbidden_symbols=[
"malloc"])
108 if not core.evaluate_against_baseline(
"new_app", bad_sym, mock_baseline):
109 failures.append(
"new forbidden malloc not flagged")
113 runtime_primitive_providers={
"memset":
"libg_nano.a(libc_a-memset.o)"},
115 if not core.evaluate_against_baseline(
"new_app", bad_prim_src, mock_baseline):
116 failures.append(
"forbidden runtime primitive provider not flagged")
118 bad_nosys = dict(clean_analysis, live_archive_members={
"libnosys.a": [
"close.o"]})
119 if not core.evaluate_against_baseline(
"new_app", bad_nosys, mock_baseline):
120 failures.append(
"live libnosys member not flagged")
122 bad_nano = dict(clean_analysis, live_archive_members={
"libg_nano.a": [
"libc_a-memset.o"]})
123 if not core.evaluate_against_baseline(
"new_app", bad_nano, mock_baseline):
124 failures.append(
"live newlib-nano member not flagged")
126 bad_posix_sym = dict(clean_analysis, posix_symbols=[
"ra8_io_stream_posix_bind"])
127 if not core.evaluate_against_baseline(
"new_app", bad_posix_sym, mock_baseline):
128 failures.append(
"forbidden host-only POSIX symbol not flagged")
130 bad_posix_obj = dict(clean_analysis, posix_providers={
"io":
"ra8_io_stream_posix.c.obj"})
131 if not core.evaluate_against_baseline(
"new_app", bad_posix_obj, mock_baseline):
132 failures.append(
"forbidden host-only POSIX provider not flagged")
133 failures.extend(_selftest_new_app_archives(mock_baseline, clean_analysis))
137def _selftest_new_app_archives(
138 mock_baseline: dict[str, Any], clean_analysis: dict[str, Any]
140 """Test allowlist policy for compiler and unknown archives."""
141 failures: list[str] = []
144 live_archive_members={
"libgcc.a": [
"_aeabi_uldivmod.o"],
"libm.a": [
"libm_a-wf_acos.o"]},
145 system_archives=[
"libgcc.a",
"libm.a"],
147 if core.evaluate_against_baseline(
"new_app", libm_ok, mock_baseline):
148 failures.append(
"approved libm member unexpectedly flagged")
150 libm_unapproved = dict(
152 live_archive_members={
"libm.a": [
"libm_a-unapproved.o"]},
153 system_archives=[
"libm.a"],
155 if not core.evaluate_against_baseline(
"new_app", libm_unapproved, mock_baseline):
156 failures.append(
"unapproved live libm member not flagged")
158 arbitrary_arch = dict(
159 clean_analysis, live_archive_members={
"libfoo.a": [
"foo.o"]}, system_archives=[
"libfoo.a"]
161 if not core.evaluate_against_baseline(
"new_app", arbitrary_arch, mock_baseline):
162 failures.append(
"arbitrary unknown live archive not flagged")
164 libm_discarded = dict(
165 clean_analysis, discarded_archive_members={
"libm.a": [
"libm_a-unapproved.o"]}
167 if core.evaluate_against_baseline(
"new_app", libm_discarded, mock_baseline):
168 failures.append(
"discarded libm member unexpectedly flagged")
170 libm_forbidden_sym = dict(libm_ok, live_forbidden_symbols=[
"malloc"])
171 if not core.evaluate_against_baseline(
"new_app", libm_forbidden_sym, mock_baseline):
172 failures.append(
"approved libm member that introduces forbidden symbol not flagged")
176def _selftest_live_archive_fail_closed(
177 mock_baseline: dict[str, Any], clean_analysis: dict[str, Any]
179 """Unknown live archives fail closed with or without system classification."""
180 failures: list[str] = []
181 unknown_live = dict(clean_analysis, live_archive_members={
"libevil.a": [
"evil.o"]})
182 if not core.evaluate_against_baseline(
"new_app", unknown_live, mock_baseline):
183 failures.append(
"unknown live archive without system tag not flagged")
185 unknown_sys = dict(unknown_live, system_archives=[
"libevil.a"])
186 if not core.evaluate_against_baseline(
"new_app", unknown_sys, mock_baseline):
187 failures.append(
"unknown live archive with system tag not flagged")
189 project_live = dict(clean_analysis, live_archive_members={
"libthreadx.a": [
"tx_x.o"]})
190 if core.evaluate_against_baseline(
"new_app", project_live, mock_baseline):
191 failures.append(
"approved project archive unexpectedly flagged")
195 runtime_primitive_providers={
"memset":
"libra8_shared_ek_ra8d2.a(m.o)"},
197 if core.evaluate_against_baseline(
"new_app", project_prim, mock_baseline):
198 failures.append(
"primitive from approved project archive unexpectedly flagged")
200 unknown_dead = dict(clean_analysis, discarded_archive_members={
"libevil.a": [
"evil.o"]})
201 if core.evaluate_against_baseline(
"new_app", unknown_dead, mock_baseline):
202 failures.append(
"discarded unknown archive unexpectedly flagged")
204 heavy_unknown = dict(
206 live_archive_members={
"libevil.a": [
"evil.o"]},
208 sbrk_provider=
"ra8_sbrk_trap.c.obj",
210 if not core.evaluate_against_baseline(
"heavy_app", heavy_unknown, mock_baseline):
211 failures.append(
"debt-baselined app with unknown live archive not flagged")
215def _selftest_debt_ratchet(mock_baseline: dict[str, Any]) -> list[str]:
216 """Test that recorded debt ratchets without authorizing new archives."""
217 failures: list[str] = []
219 "live_forbidden_symbols": [
"malloc",
"free"],
220 "live_archive_members": {
"libnosys.a": [
"close.o"]},
221 "sbrk_present":
True,
222 "sbrk_provider":
"ra8_sbrk_trap.c.obj",
223 "end_symbol_present":
False,
224 "has_heap_section":
False,
226 if core.evaluate_against_baseline(
"heavy_app", heavy_ok, mock_baseline):
227 failures.append(
"heavy app within baseline flagged")
229 heavy_realloc = dict(heavy_ok, live_forbidden_symbols=[
"malloc",
"free",
"realloc"])
230 if not core.evaluate_against_baseline(
"heavy_app", heavy_realloc, mock_baseline):
231 failures.append(
"debt expansion not flagged")
237 live_archive_members={
"libnosys.a": [
"close.o"],
"libm.a": [
"libm_a-unapproved.o"]},
238 system_archives=[
"libm.a"],
240 if not core.evaluate_against_baseline(
"heavy_app", heavy_libm, mock_baseline):
241 failures.append(
"debt-baselined app with unapproved libm member not flagged")
243 heavy_unknown = dict(
245 live_archive_members={
"libfoo.a": [
"foo.o"]},
246 system_archives=[
"libfoo.a"],
248 if not core.evaluate_against_baseline(
"heavy_app", heavy_unknown, mock_baseline):
249 failures.append(
"debt-baselined app with unknown live archive not flagged")
253def _selftest_zero_debt(clean_analysis: dict[str, Any]) -> list[str]:
254 """Test that zero-debt baselines enforce strict invariants."""
255 failures: list[str] = []
256 zero_debt_baseline = {
259 "forbidden_symbols": [],
260 "forbidden_archives": {},
261 "sbrk_provider":
"none",
267 if core.evaluate_against_baseline(
"clean_app", clean_analysis, zero_debt_baseline):
268 failures.append(
"clean zero-debt baselined app unexpectedly flagged")
270 bad_zero_prim = dict(
272 runtime_primitive_providers={
"memset":
"libg_nano.a(libc_a-memset.o)"},
274 bad_zero_sym = dict(clean_analysis, live_forbidden_symbols=[
"malloc"])
275 if not core.evaluate_against_baseline(
"clean_app", bad_zero_sym, zero_debt_baseline):
276 failures.append(
"zero-debt baselined app with forbidden symbol not flagged")
278 bad_zero_arch = dict(clean_analysis, live_archive_members={
"libnosys.a": [
"close.o"]})
279 if not core.evaluate_against_baseline(
"clean_app", bad_zero_arch, zero_debt_baseline):
280 failures.append(
"zero-debt baselined app with forbidden archive member not flagged")
282 bad_zero_end = dict(clean_analysis, end_symbol_present=
True)
283 if not core.evaluate_against_baseline(
"clean_app", bad_zero_end, zero_debt_baseline):
284 failures.append(
"zero-debt baselined app with end symbol not flagged")
286 bad_zero_heap = dict(clean_analysis, has_heap_section=
True)
287 if not core.evaluate_against_baseline(
"clean_app", bad_zero_heap, zero_debt_baseline):
288 failures.append(
"zero-debt baselined app with .heap section not flagged")
290 if not core.evaluate_against_baseline(
"clean_app", bad_zero_prim, zero_debt_baseline):
292 "zero-debt baselined app with runtime primitive from libg_nano.a not flagged"
295 bad_zero_sbrk = dict(
298 sbrk_provider=
"ra8_sbrk_trap.c.obj",
300 if not core.evaluate_against_baseline(
"clean_app", bad_zero_sbrk, zero_debt_baseline):
301 failures.append(
"zero-debt baselined app with newly-live _sbrk not flagged")
303 bad_zero_posix = dict(clean_analysis, posix_symbols=[
"ra8_io_stream_posix_bind"])
304 if not core.evaluate_against_baseline(
"clean_app", bad_zero_posix, zero_debt_baseline):
305 failures.append(
"zero-debt baselined app with POSIX symbol not flagged")
307 bad_zero_posix_obj = dict(clean_analysis, posix_providers={
"io":
"ra8_io_stream_posix.c.obj"})
308 if not core.evaluate_against_baseline(
"clean_app", bad_zero_posix_obj, zero_debt_baseline):
309 failures.append(
"zero-debt baselined app with POSIX provider not flagged")
314def _selftest_ratchet_eval() -> list[str]:
315 """Test baseline ratchet evaluation."""
319 "forbidden_symbols": [
"malloc",
"free"],
320 "forbidden_archives": {
"libnosys.a": [
"close.o"]},
321 "sbrk_provider":
"ra8_sbrk_trap.c.obj",
328 "live_forbidden_symbols": [],
329 "live_archive_members": {
"libgcc.a": [
"_aeabi_uldivmod.o"]},
330 "system_archives": [
"libgcc.a"],
331 "runtime_primitive_providers": {
"memset":
"ra8_freestanding_mem.c.obj"},
332 "sbrk_present":
False,
333 "sbrk_provider":
"none",
334 "end_symbol_present":
False,
335 "has_heap_section":
False,
338 _selftest_new_app_policy(mock_baseline, clean_analysis)
339 + _selftest_live_archive_fail_closed(mock_baseline, clean_analysis)
340 + _selftest_debt_ratchet(mock_baseline)
341 + _selftest_zero_debt(clean_analysis)
345def _selftest_scripts_and_ratchet() -> list[str]:
346 """Test linker script verification and baseline ratchet evaluation."""
347 return _selftest_linker_scripts() + _selftest_ratchet_eval()
350def _selftest_source_asserts(repo_root: pathlib.Path) -> list[str]:
351 """Test source assert scanner."""
352 failures: list[str] = []
353 clean_src =
'void f(void) { static_assert(1, "ok"); RA8_ASSERT(x > 0, "msg"); }'
354 bad_inc =
"#include <assert.h>\nvoid f(void) {}"
355 bad_call =
"void f(int x) { assert(x > 0); }"
357 if core.check_source_asserts(clean_src,
"clean.c"):
358 failures.append(
"clean source with static_assert/RA8_ASSERT flagged")
359 if not core.check_source_asserts(bad_inc,
"bad_inc.c"):
360 failures.append(
"<assert.h> include not flagged")
361 if not core.check_source_asserts(bad_call,
"bad_call.c"):
362 failures.append(
"runtime assert() call not flagged")
365 esp_hosted_bad =
"void f(void) { assert(bus != NULL); }"
366 if not core.check_source_asserts(esp_hosted_bad,
"port/esp-hosted/src/ra8_esp_hosted_spi.c"):
367 failures.append(
"target esp-hosted assert() not flagged")
370 '#include "ra8_check.h"\n'
372 '#define assert(expr) RA8_ASSERT(expr, "msg")\n'
375 if core.check_source_asserts(macro_redirect,
"port/esp-hosted/inc/port_esp_hosted_host_os.h"):
376 failures.append(
"macro redirect #define assert(...) incorrectly flagged")
380 posix_header = repo_root /
"port" /
"posix" /
"inc" /
"ra8_io_stream_posix.h"
381 if not posix_header.is_file():
382 failures.append(
"port/posix/inc/ra8_io_stream_posix.h not found for guard check")
384 text = posix_header.read_text(encoding=
"utf-8")
385 if "#ifndef RA8_OFF_TARGET" not in text
or "#error" not in text:
386 failures.append(
"port/posix/inc/ra8_io_stream_posix.h missing fail-closed guard")
391def _selftest_provider_policy(
392 mock_baseline: dict[str, Any], clean_analysis: dict[str, Any]
394 """Test primitive-provider and project-archive policy for unknown apps."""
395 failures: list[str] = []
398 live_archive_members={
"libthreadx.a": [
"tx_thread_create.c.obj"]},
401 if core.evaluate_against_baseline(
"new_app", project_arch, mock_baseline):
402 failures.append(
"project-built archive unexpectedly flagged")
406 runtime_primitive_providers={
"memcpy":
"libfoo.a(foo.o)"},
408 if not core.evaluate_against_baseline(
"new_app", prim_unknown, mock_baseline):
409 failures.append(
"primitive resolved to unknown archive not flagged")
411 prim_libm_bad = dict(
413 live_archive_members={
"libm.a": [
"libm_a-unapproved.o"]},
414 system_archives=[
"libm.a"],
415 runtime_primitive_providers={
"strlen":
"libm.a(libm_a-unapproved.o)"},
417 if not core.evaluate_against_baseline(
"new_app", prim_libm_bad, mock_baseline):
418 failures.append(
"primitive resolved to unapproved libm member not flagged")
422 live_archive_members={
"libm.a": [
"libm_a-wf_acos.o"]},
423 system_archives=[
"libm.a"],
424 runtime_primitive_providers={
"strlen":
"ra8_freestanding_str.c.obj"},
426 if core.evaluate_against_baseline(
"new_app", prim_libm_ok, mock_baseline):
427 failures.append(
"project-resolved primitive with live libm unexpectedly flagged")
431 runtime_primitive_providers={
"memset":
".../libgcc.a(_arm_muldf3.o)"},
433 if core.evaluate_against_baseline(
"new_app", prim_pathed, mock_baseline):
434 failures.append(
"path-form allowed archive provider unexpectedly flagged")
438def _selftest_db_flags() -> list[str]:
439 """Test RA8_FREESTANDING compile-database enforcement both directions."""
440 failures: list[str] = []
441 flagged = [{
"file":
"a.c",
"command":
"cc -DRA8_FREESTANDING -c a.c"}]
442 if core.check_db_entries(flagged, must_have=
True, exempt=
None, label=
"Target"):
443 failures.append(
"fully flagged target database unexpectedly flagged")
444 if not core.check_target_db_flags(pathlib.Path(
"nonexistent-db.json")):
445 failures.append(
"missing target database path not flagged")
446 missing_flag = [*flagged, {
"file":
"b.c",
"command":
"cc -O2 -c b.c"}]
447 if not core.check_db_entries(missing_flag, must_have=
True, exempt=
None, label=
"Target"):
448 failures.append(
"target database entry missing the flag not flagged")
450 stray_flag = [{
"file":
"h.c",
"command":
"cc -DRA8_FREESTANDING -c h.c"}]
451 if not core.check_db_entries(
452 stray_flag, must_have=
False, exempt=
"-DRA8_TEST_FREESTANDING", label=
"Host"
454 failures.append(
"host database entry setting the flag not flagged")
457 {
"file":
"t.c",
"command":
"cc -DRA8_FREESTANDING -DRA8_TEST_FREESTANDING -c t.c"}
459 if core.check_db_entries(
460 exempt_flag, must_have=
False, exempt=
"-DRA8_TEST_FREESTANDING", label=
"Host"
462 failures.append(
"exempt freestanding-test host entry unexpectedly flagged")
464 with tempfile.TemporaryDirectory()
as tmp:
465 root = pathlib.Path(tmp)
466 db_dir = root /
"examples" /
"demo" /
"build"
467 db_dir.mkdir(parents=
True)
468 db_path = db_dir /
"compile_commands.json"
469 db_path.write_text(json.dumps(flagged), encoding=
"utf-8")
470 if core.discover_target_dbs(root) != [db_path]:
471 failures.append(
"per-app target database not discovered")
472 if core.check_discovered_target_dbs(root):
473 failures.append(
"flagged per-app target database unexpectedly flagged")
474 db_path.write_text(json.dumps(missing_flag), encoding=
"utf-8")
475 if not core.check_discovered_target_dbs(root):
476 failures.append(
"unflagged per-app target database not flagged")
477 db_path.write_text(
"[]", encoding=
"utf-8")
478 if not core.check_discovered_target_dbs(root):
479 failures.append(
"empty per-app target database not flagged")
480 if not core.check_target_db_flags(root /
"examples" /
"demo" /
"none.json"):
481 failures.append(
"missing target database not flagged")
485def run_selftests(repo_root: pathlib.Path) -> list[str]:
486 """Run every freestanding-runtime selftest; return failure strings."""
487 failures = _selftest_nm_and_map() + _selftest_scripts_and_ratchet()
488 failures.extend(_selftest_source_asserts(repo_root))
489 failures.extend(_selftest_db_flags())
490 mock_baseline: dict[str, Any] = {
"apps": {}}
491 clean_analysis: dict[str, Any] = {
492 "live_forbidden_symbols": [],
493 "live_archive_members": {
"libgcc.a": [
"_aeabi_uldivmod.o"]},
494 "system_archives": [
"libgcc.a"],
495 "runtime_primitive_providers": {
"memset":
"ra8_freestanding_mem.c.obj"},
496 "sbrk_present":
False,
497 "sbrk_provider":
"none",
498 "end_symbol_present":
False,
499 "has_heap_section":
False,
501 failures.extend(_selftest_provider_policy(mock_baseline, clean_analysis))