brickproof-fw 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. brickproof/__init__.py +3 -0
  2. brickproof/adapter.py +399 -0
  3. brickproof/cli.py +304 -0
  4. brickproof/demo/README.md +23 -0
  5. brickproof/demo/__init__.py +3 -0
  6. brickproof/demo/adapter.yaml +59 -0
  7. brickproof/demo/bug-consume_first.yaml +10 -0
  8. brickproof/demo/bug-early_unpg.yaml +11 -0
  9. brickproof/demo/bug-naive_floor.yaml +10 -0
  10. brickproof/demo/bug-no_erase.yaml +11 -0
  11. brickproof/demo/bug-no_unlock.yaml +11 -0
  12. brickproof/demo/bug-pop_after_switch.yaml +11 -0
  13. brickproof/demo/bug-protected_slot.yaml +9 -0
  14. brickproof/demo/bug-resume_copy.yaml +13 -0
  15. brickproof/demo/bug-retry_limit.yaml +14 -0
  16. brickproof/demo/bug-self_lock.yaml +14 -0
  17. brickproof/demo/bug-trust_header.yaml +10 -0
  18. brickproof/demo/build.py +101 -0
  19. brickproof/demo/f411-bug-consume_first.yaml +10 -0
  20. brickproof/demo/f411-bug-early_unpg.yaml +11 -0
  21. brickproof/demo/f411-bug-naive_floor.yaml +10 -0
  22. brickproof/demo/f411-bug-no_erase.yaml +12 -0
  23. brickproof/demo/f411-bug-no_unlock.yaml +11 -0
  24. brickproof/demo/f411-bug-pop_after_switch.yaml +11 -0
  25. brickproof/demo/f411-bug-protected_slot.yaml +9 -0
  26. brickproof/demo/f411-bug-retry_limit.yaml +14 -0
  27. brickproof/demo/f411-bug-self_lock.yaml +14 -0
  28. brickproof/demo/f411-bug-trust_header.yaml +10 -0
  29. brickproof/demo/f411-bug-watchdog.yaml +14 -0
  30. brickproof/demo/f411-hold_pg.yaml +11 -0
  31. brickproof/demo/f411-watchdog.yaml +7 -0
  32. brickproof/demo/f411.yaml +62 -0
  33. brickproof/demo/hold_pg.yaml +11 -0
  34. brickproof/demo/prebuilt/app_v1.img +0 -0
  35. brickproof/demo/prebuilt/app_v2.img +0 -0
  36. brickproof/demo/prebuilt/bootloader.elf +0 -0
  37. brickproof/demo/prebuilt/bootloader_bug_consume_first.elf +0 -0
  38. brickproof/demo/prebuilt/bootloader_bug_early_unpg.elf +0 -0
  39. brickproof/demo/prebuilt/bootloader_bug_naive_floor.elf +0 -0
  40. brickproof/demo/prebuilt/bootloader_bug_no_erase.elf +0 -0
  41. brickproof/demo/prebuilt/bootloader_bug_no_unlock.elf +0 -0
  42. brickproof/demo/prebuilt/bootloader_bug_pop_after_switch.elf +0 -0
  43. brickproof/demo/prebuilt/bootloader_bug_resume_copy.elf +0 -0
  44. brickproof/demo/prebuilt/bootloader_bug_retry_limit.elf +0 -0
  45. brickproof/demo/prebuilt/bootloader_bug_self_lock.elf +0 -0
  46. brickproof/demo/prebuilt/bootloader_bug_trust_header.elf +0 -0
  47. brickproof/demo/prebuilt/bootloader_hold_pg.elf +0 -0
  48. brickproof/demo/prebuilt/f411_app_v1.img +0 -0
  49. brickproof/demo/prebuilt/f411_app_v2.img +0 -0
  50. brickproof/demo/prebuilt/f411_bootloader.elf +0 -0
  51. brickproof/demo/prebuilt/f411_bootloader_bug_consume_first.elf +0 -0
  52. brickproof/demo/prebuilt/f411_bootloader_bug_early_unpg.elf +0 -0
  53. brickproof/demo/prebuilt/f411_bootloader_bug_naive_floor.elf +0 -0
  54. brickproof/demo/prebuilt/f411_bootloader_bug_no_erase.elf +0 -0
  55. brickproof/demo/prebuilt/f411_bootloader_bug_no_unlock.elf +0 -0
  56. brickproof/demo/prebuilt/f411_bootloader_bug_pop_after_switch.elf +0 -0
  57. brickproof/demo/prebuilt/f411_bootloader_bug_retry_limit.elf +0 -0
  58. brickproof/demo/prebuilt/f411_bootloader_bug_self_lock.elf +0 -0
  59. brickproof/demo/prebuilt/f411_bootloader_bug_trust_header.elf +0 -0
  60. brickproof/demo/prebuilt/f411_bootloader_bug_watchdog.elf +0 -0
  61. brickproof/demo/prebuilt/f411_bootloader_hold_pg.elf +0 -0
  62. brickproof/demo/prebuilt/meta_floor1.bin +0 -0
  63. brickproof/demo/watchdog.yaml +7 -0
  64. brickproof/drivers/__init__.py +1 -0
  65. brickproof/drivers/staged_slot.py +91 -0
  66. brickproof/engine/__init__.py +0 -0
  67. brickproof/engine/session.py +724 -0
  68. brickproof/engine/workdir.py +167 -0
  69. brickproof/models/mem_guard.py +21 -0
  70. brickproof/models/stm32f1_flash.py +435 -0
  71. brickproof/models/stm32f1_rcc.py +17 -0
  72. brickproof/models/stm32f4_flash.py +435 -0
  73. brickproof/oracle.py +105 -0
  74. brickproof/plan.py +137 -0
  75. brickproof/platforms/stm32f103.repl +34 -0
  76. brickproof/platforms/stm32f411.repl +20 -0
  77. brickproof/pool.py +79 -0
  78. brickproof/regulations.py +137 -0
  79. brickproof/report.py +287 -0
  80. brickproof/runner.py +638 -0
  81. brickproof/torn.py +101 -0
  82. brickproof/trace.py +186 -0
  83. brickproof_fw-0.1.0.dist-info/METADATA +249 -0
  84. brickproof_fw-0.1.0.dist-info/RECORD +88 -0
  85. brickproof_fw-0.1.0.dist-info/WHEEL +5 -0
  86. brickproof_fw-0.1.0.dist-info/entry_points.txt +2 -0
  87. brickproof_fw-0.1.0.dist-info/licenses/LICENSE +202 -0
  88. brickproof_fw-0.1.0.dist-info/top_level.txt +1 -0
brickproof/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """BrickProof: power-fail testing for firmware updates."""
2
+
3
+ __version__ = "0.1.0"
brickproof/adapter.py ADDED
@@ -0,0 +1,399 @@
1
+ """Target adapters: one YAML file describing the firmware, plus a Python driver.
2
+
3
+ The YAML says what to load, the flash geometry, how to reset, and where to cut.
4
+ The driver knows the target's update protocol (how to stage and start an update,
5
+ how to tell the device is alive). See adapters/three_tier_f103/ for an example.
6
+ """
7
+ import importlib.util
8
+ import os
9
+ from dataclasses import dataclass, field
10
+
11
+ import yaml
12
+
13
+ from brickproof.engine.session import FlashGeometry
14
+ from brickproof.torn import KINDS as TORN_KINDS
15
+
16
+
17
+ class AdapterError(ValueError):
18
+ pass
19
+
20
+
21
+ @dataclass
22
+ class Cut:
23
+ """One place to pull the plug.
24
+
25
+ on_erase cut just before the erase of this page address starts
26
+ at_op cut just before flash operation number N, counted from the start
27
+ of the update (0 = its first erase or program)
28
+ expect optional (kind, address) the cut must land on, as a sanity check
29
+ group cuts expanded from one at_ops range share a group; once one of them
30
+ finds the update already over, the later ones are skipped
31
+ """
32
+ name: str
33
+ on_erase: int = None
34
+ at_op: int = None
35
+ expect: tuple = None
36
+ group: int = None
37
+
38
+
39
+ @dataclass
40
+ class Adapter:
41
+ path: str # the YAML file it was read from
42
+ name: str
43
+ folder: str
44
+ target: str
45
+ platform: str
46
+ platform_extra: str
47
+ flash: FlashGeometry
48
+ ram_base: int
49
+ ram_size: int
50
+ firmware: list
51
+ uart: str
52
+ vector_table: int
53
+ mips: int
54
+ reset_cmds: list
55
+ boot_settle: float
56
+ cut_limit: float
57
+ update_time: float # virtual seconds an uninterrupted update needs
58
+ check_states: bool # compare flash at each cut with the golden run replayed
59
+ oracle: dict = None # approved images and which ones a cut may leave (see oracle.py)
60
+ plan: dict = None # let the planner choose cuts from the golden run (see plan.py)
61
+ torn: list = None # torn-operation shapes to try at each cut (see torn.py)
62
+ torn_seed: int = 0
63
+ depth: dict = None # explore cuts during recovery (see runner.explore)
64
+ shard: tuple = None # (k, n): run only every nth cut, starting with the kth
65
+ driver_file: str = None # a driver.py next to the adapter, or
66
+ driver_builtin: str = None # one that ships with BrickProof
67
+ driver_options: dict = field(default_factory=dict)
68
+ cuts: list = field(default_factory=list)
69
+
70
+ def load_driver(self, session):
71
+ """Import the driver module and return its driver object for this session."""
72
+ if self.driver_builtin:
73
+ mod = importlib.import_module("brickproof.drivers." + self.driver_builtin)
74
+ else:
75
+ spec = importlib.util.spec_from_file_location("brickproof_driver_" + self.name, self.driver_file)
76
+ mod = importlib.util.module_from_spec(spec)
77
+ spec.loader.exec_module(mod)
78
+ return mod.make_driver(session, self)
79
+
80
+
81
+ def _protected_ranges(flash, protected):
82
+ """The write_protected entries as checked (where, start, size) tuples."""
83
+ out = []
84
+ for i, r in enumerate(protected):
85
+ where = "flash.write_protected[%d]" % i
86
+ start, size = _need(r, "from", where), _need(r, "size", where)
87
+ if start < flash.base or start + size > flash.base + flash.size or size <= 0:
88
+ raise AdapterError("%s: outside the flash" % where)
89
+ out.append((where, start, size))
90
+ return out
91
+
92
+
93
+ def stm32f1_option_bytes(flash, protected, read_protected, hardware_watchdog=False):
94
+ """The 16 option bytes of an STM32F1 as the chip holds them: RDP, USER, Data0,
95
+ Data1, WRP0..WRP3, each followed by its complement. `protected` is a list of
96
+ {from, size} ranges to write-protect; a cleared WRP bit protects its unit."""
97
+ unit = flash.wrp_unit
98
+ wrp = 0xFFFFFFFF
99
+ for where, start, size in _protected_ranges(flash, protected):
100
+ if (start - flash.base) % unit or size % unit:
101
+ raise AdapterError("%s: write protection goes in units of 0x%X on this chip"
102
+ % (where, unit))
103
+ for u in range((start - flash.base) // unit, (start - flash.base + size) // unit):
104
+ if u >= 31 and (start - flash.base + size) < flash.size:
105
+ raise AdapterError("%s: the last protection bit covers everything from 0x%08X "
106
+ "to the end of flash, all or nothing" % (where, flash.base + 31 * unit))
107
+ wrp &= ~(1 << min(u, 31))
108
+ out = bytearray()
109
+ user = 0xFE if hardware_watchdog else 0xFF # WDG_SW is bit 0, cleared = hardware
110
+ for data in (0x00 if read_protected else 0xA5, user, 0xFF, 0xFF,
111
+ wrp & 0xFF, (wrp >> 8) & 0xFF, (wrp >> 16) & 0xFF, (wrp >> 24) & 0xFF):
112
+ out += bytes([data, ~data & 0xFF])
113
+ return bytes(out)
114
+
115
+
116
+ def stm32f4_option_bytes(flash, protected, read_protected, hardware_watchdog=False):
117
+ """The option bytes of an STM32F4 as the chip shows them: USER and RDP in one
118
+ halfword, the nWRP bits in another, each followed by its complement. Write
119
+ protection is one bit per sector, and a cleared bit protects its sector."""
120
+ units = flash.units()
121
+ nwrp = 0xFFF
122
+ for where, start, size in _protected_ranges(flash, protected):
123
+ starts = [u for u, _ in units]
124
+ ends = [u + n for u, n in units]
125
+ if start not in starts or start + size not in ends:
126
+ raise AdapterError("%s: write protection goes by whole sectors on this chip" % where)
127
+ for s in range(starts.index(start), ends.index(start + size) + 1):
128
+ nwrp &= ~(1 << s)
129
+ out = bytearray()
130
+ user = 0xCF if hardware_watchdog else 0xEF # WDG_SW is bit 5, cleared = hardware
131
+ for half in (((0x55 if read_protected else 0xAA) << 8) | user, 0xF000 | nwrp):
132
+ out += bytes([half & 0xFF, half >> 8, ~half & 0xFF, (~half >> 8) & 0xFF]) + bytes([0xFF]) * 4
133
+ return bytes(out)
134
+
135
+
136
+ # What the engine has to know about each chip family it can emulate.
137
+ # model the flash controller model in models/
138
+ # controller where that controller sits
139
+ # ob_base where the option bytes are
140
+ # option_bytes builds them from an adapter's write_protected / read_protected
141
+ # wrp_unit how much flash one write-protection bit covers (None: one per sector)
142
+ # write_unit bytes a program usually writes
143
+ # endurance erase cycles per page or sector the datasheet promises
144
+ CHIPS = {
145
+ "stm32f103": dict(model="stm32f1_flash.py", controller=0x40022000, ob_base=0x1FFFF800,
146
+ option_bytes=stm32f1_option_bytes, wrp_unit=0x1000, write_unit=2,
147
+ endurance=10000),
148
+ "stm32f411": dict(model="stm32f4_flash.py", controller=0x40023C00, ob_base=0x1FFFC000,
149
+ option_bytes=stm32f4_option_bytes, wrp_unit=None, write_unit=4,
150
+ endurance=10000),
151
+ }
152
+ PLATFORMS = tuple(CHIPS)
153
+
154
+ # flash.timing: how long erases and programs take in the emulator
155
+ TIMINGS = {"none": 0, "typical": 1, "max": 2}
156
+
157
+
158
+ def _need(d, key, where):
159
+ if key not in d:
160
+ raise AdapterError("%s: missing '%s'" % (where, key))
161
+ return d[key]
162
+
163
+
164
+ def _parse_cuts(i, c):
165
+ """One YAML entry: a single cut, or an at_ops range that expands to many."""
166
+ where = "cuts[%d]" % i
167
+ if not isinstance(c, dict):
168
+ raise AdapterError("%s: expected a mapping" % where)
169
+ if "at_ops" in c:
170
+ r = c["at_ops"]
171
+ lo, hi, step = r.get("from", 0), r.get("to"), r.get("step", 1)
172
+ if hi is None or hi < lo or step < 1:
173
+ raise AdapterError("%s.at_ops: need from <= to and step >= 1" % where)
174
+ return [Cut(name="op-%d" % n, at_op=n, group=i) for n in range(lo, hi + 1, step)]
175
+ return [_parse_cut(where, c)]
176
+
177
+
178
+ def _parse_cut(where, c):
179
+ on_erase, at_op = c.get("on_erase"), c.get("at_op")
180
+ if (on_erase is None) == (at_op is None):
181
+ raise AdapterError("%s: give exactly one of on_erase or at_op" % where)
182
+ expect = None
183
+ if "expect" in c:
184
+ e = c["expect"]
185
+ kind = {"erase": 1, "program": 2}.get(e.get("kind"))
186
+ if kind is None:
187
+ raise AdapterError("%s.expect.kind: use erase or program" % where)
188
+ expect = (kind, e.get("addr", 0))
189
+ default = ("erase-0x%08X" % on_erase) if on_erase is not None else "op-%d" % at_op
190
+ return Cut(name=c.get("name", default), on_erase=on_erase, at_op=at_op, expect=expect)
191
+
192
+
193
+ def _merge(base, over):
194
+ """Mappings merge key by key, anything else (lists included) is replaced."""
195
+ out = dict(base)
196
+ for k, v in over.items():
197
+ out[k] = _merge(out[k], v) if isinstance(v, dict) and isinstance(out.get(k), dict) else v
198
+ return out
199
+
200
+
201
+ def _read(path, depth=0):
202
+ with open(path) as f:
203
+ d = yaml.safe_load(f) or {}
204
+ parent = d.pop("extends", None)
205
+ if parent:
206
+ if depth > 4:
207
+ raise AdapterError("%s: 'extends' nests too deep" % path)
208
+ d = _merge(_read(os.path.join(os.path.dirname(path), parent), depth + 1), d)
209
+ return d
210
+
211
+
212
+ def _parse_plan(scenario):
213
+ """scenario.plan: a mode name, or {mode, per_phase, small, real_every}. With
214
+ real_every N only every Nth planned cut is made in the emulator; the others are
215
+ powered up from the computed flash state. None if absent."""
216
+ p = scenario.get("plan")
217
+ if p is None:
218
+ return None
219
+ if isinstance(p, str):
220
+ p = {"mode": p}
221
+ if not isinstance(p, dict):
222
+ raise AdapterError("scenario.plan: give a mode (phases, every) or a mapping")
223
+ unknown = set(p) - {"mode", "per_phase", "small", "real_every"}
224
+ if unknown:
225
+ raise AdapterError("scenario.plan: unknown key(s) %s" % ", ".join(sorted(unknown)))
226
+ mode = p.get("mode", "phases")
227
+ if mode not in ("phases", "every"):
228
+ raise AdapterError("scenario.plan.mode: use phases or every, not %r" % mode)
229
+ out = {"mode": mode}
230
+ for key in ("per_phase", "small", "real_every"):
231
+ if key in p:
232
+ if not isinstance(p[key], int) or p[key] < 0:
233
+ raise AdapterError("scenario.plan.%s: a whole number, 0 or more" % key)
234
+ out[key] = p[key]
235
+ return out
236
+
237
+
238
+ DEPTH_DEFAULTS = {"mode": "phases", "per_phase": 2, "small": 6, "max_states": 2000, "torn": False}
239
+
240
+
241
+ def _parse_depth(scenario):
242
+ """scenario.depth: how many power cuts in a row to explore. A number, or a mapping
243
+ {levels, mode, per_phase, small, max_states, torn}. With torn, the half-done
244
+ versions of each recovery operation are tried as well (it needs scenario.torn or
245
+ --torn for the shapes). None when it is 1 or absent."""
246
+ d = scenario.get("depth")
247
+ if d is None:
248
+ return None
249
+ if isinstance(d, int) and not isinstance(d, bool):
250
+ d = {"levels": d}
251
+ if not isinstance(d, dict):
252
+ raise AdapterError("scenario.depth: a number of cuts in a row, or a mapping")
253
+ unknown = set(d) - {"levels", *DEPTH_DEFAULTS}
254
+ if unknown:
255
+ raise AdapterError("scenario.depth: unknown key(s) %s" % ", ".join(sorted(unknown)))
256
+ out = dict(DEPTH_DEFAULTS, **d)
257
+ out.setdefault("levels", 2)
258
+ if out["mode"] not in ("phases", "every"):
259
+ raise AdapterError("scenario.depth.mode: use phases or every, not %r" % out["mode"])
260
+ for key in ("levels", "per_phase", "small", "max_states"):
261
+ if not isinstance(out[key], int) or isinstance(out[key], bool) or out[key] < 0:
262
+ raise AdapterError("scenario.depth.%s: a whole number, 0 or more" % key)
263
+ if not isinstance(out["torn"], bool):
264
+ raise AdapterError("scenario.depth.torn: true or false")
265
+ if out["levels"] < 1:
266
+ raise AdapterError("scenario.depth.levels: at least 1")
267
+ return out if out["levels"] > 1 else None
268
+
269
+
270
+ def _parse_torn(scenario):
271
+ """scenario.torn: true for every shape, or a list of shape names. None if off."""
272
+ t = scenario.get("torn")
273
+ if not t:
274
+ return None
275
+ if t is True:
276
+ return list(TORN_KINDS)
277
+ if not isinstance(t, list) or any(k not in TORN_KINDS for k in t):
278
+ raise AdapterError("scenario.torn: true, or a list from: %s" % ", ".join(TORN_KINDS))
279
+ return list(t)
280
+
281
+
282
+ def _parse_oracle(d, target):
283
+ """The `oracle:` section, with image paths made absolute. None if there is none."""
284
+ o = d.get("oracle")
285
+ if not o:
286
+ return None
287
+ images = {}
288
+ for name, spec in (o.get("images") or {}).items():
289
+ where = "oracle.images.%s" % name
290
+ path = os.path.normpath(os.path.join(target, _need(spec, "file", where)))
291
+ images[str(name)] = (path, _need(spec, "at", where))
292
+ accept = [str(a) for a in _need(o, "accept", "oracle")]
293
+ if not accept:
294
+ raise AdapterError("oracle.accept: name at least one image")
295
+ run_in = o.get("run_in")
296
+ if run_in is not None:
297
+ run_in = (_need(run_in, "start", "oracle.run_in"), _need(run_in, "end", "oracle.run_in"))
298
+ if run_in[0] >= run_in[1]:
299
+ raise AdapterError("oracle.run_in: start must be below end")
300
+ return {"images": images, "accept": accept, "run_in": run_in,
301
+ "samples": int(o.get("samples", 4)), "interval": float(o.get("interval", 0.02))}
302
+
303
+
304
+ def load(path, target=None):
305
+ """Read an adapter YAML. `target` overrides the firmware project folder it names.
306
+
307
+ `extends: other.yaml` starts from another adapter and changes only what this
308
+ file says. Relative paths resolve against this file's folder, so keep the two
309
+ side by side.
310
+ """
311
+ path = os.path.abspath(path)
312
+ folder = os.path.dirname(path)
313
+ d = _read(path)
314
+
315
+ name = _need(d, "name", path)
316
+ target = os.path.abspath(target or os.path.join(folder, _need(d, "target", path)))
317
+
318
+ plat = _need(d, "platform", path)
319
+ base = _need(plat, "base", "platform")
320
+ if base not in PLATFORMS:
321
+ raise AdapterError("platform.base: unknown '%s' (have: %s)" % (base, ", ".join(PLATFORMS)))
322
+ extra = plat.get("extra")
323
+
324
+ fl = _need(d, "flash", path)
325
+ chip = CHIPS[base]
326
+ flash = FlashGeometry(base=_need(fl, "base", "flash"), size=_need(fl, "size", "flash"),
327
+ page=fl.get("page"), write_unit=fl.get("write_unit", chip["write_unit"]),
328
+ controller=chip["controller"], model=chip["model"], ob_base=chip["ob_base"],
329
+ wrp_unit=chip["wrp_unit"], endurance=chip["endurance"])
330
+ timing = fl.get("timing", "typical")
331
+ if timing not in TIMINGS:
332
+ raise AdapterError("flash.timing: unknown '%s' (have: %s)" % (timing, ", ".join(TIMINGS)))
333
+ flash.timing = TIMINGS[timing]
334
+ if (flash.page is None) == ("sectors" not in fl):
335
+ raise AdapterError("flash: give exactly one of page (one erase size everywhere) "
336
+ "or sectors (a list of sizes)")
337
+ if flash.page is not None:
338
+ if flash.page <= 0 or flash.size % flash.page:
339
+ raise AdapterError("flash.size must be a whole number of pages")
340
+ else:
341
+ sizes = fl["sectors"]
342
+ if not isinstance(sizes, list) or not sizes or any(not isinstance(n, int) or n <= 0 for n in sizes):
343
+ raise AdapterError("flash.sectors must be a list of sector sizes")
344
+ if sum(sizes) != flash.size:
345
+ raise AdapterError("flash.sectors add up to 0x%X, but flash.size is 0x%X"
346
+ % (sum(sizes), flash.size))
347
+ flash.sectors = [(sum(sizes[:i]), n) for i, n in enumerate(sizes)]
348
+ if chip["model"] == "stm32f1_flash.py" and flash.page is None:
349
+ raise AdapterError("flash: an %s erases by page, not by sector" % base)
350
+ if chip["model"] == "stm32f4_flash.py" and flash.sectors is None:
351
+ raise AdapterError("flash: an %s erases by sector; list them in flash.sectors" % base)
352
+ flash.option_bytes = chip["option_bytes"](flash, fl.get("write_protected") or [],
353
+ bool(fl.get("read_protected", False)),
354
+ bool(fl.get("hardware_watchdog", False)))
355
+
356
+ # RAM size is not optional: an emulator with more RAM than the chip hides
357
+ # real faults (a stack access past the end of RAM just works)
358
+ ram = _need(d, "ram", path)
359
+ ram_size = _need(ram, "size", "ram")
360
+ if ram_size <= 0:
361
+ raise AdapterError("ram.size must be positive")
362
+
363
+ firmware = []
364
+ for i, fw in enumerate(_need(d, "firmware", path)):
365
+ file = _need(fw, "file", "firmware[%d]" % i)
366
+ addr = fw.get("addr")
367
+ if not file.lower().endswith(".elf") and addr is None:
368
+ raise AdapterError("firmware[%d]: a raw binary needs 'addr'" % i)
369
+ firmware.append((os.path.normpath(os.path.join(target, file)), addr))
370
+
371
+ cpu = d.get("cpu", {})
372
+ drv = _need(d, "driver", path)
373
+ scenario = d.get("scenario") or {}
374
+ cuts = [cut for i, c in enumerate(scenario.get("cuts") or []) for cut in _parse_cuts(i, c)]
375
+ if bool(drv.get("file")) == bool(drv.get("builtin")):
376
+ raise AdapterError("driver: give exactly one of file or builtin")
377
+
378
+ return Adapter(
379
+ path=path, name=name, folder=folder, target=target,
380
+ platform=base, platform_extra=os.path.join(folder, extra) if extra else None,
381
+ flash=flash, ram_base=ram.get("base", 0x20000000), ram_size=ram_size, firmware=firmware,
382
+ uart=d.get("uart", "usart2"),
383
+ vector_table=cpu.get("vector_table", flash.base),
384
+ mips=cpu.get("mips", 72),
385
+ reset_cmds=list(d.get("reset", [])),
386
+ boot_settle=float(d.get("boot_settle", 1.5)),
387
+ cut_limit=float(scenario.get("cut_limit", 30.0)),
388
+ update_time=float(scenario.get("update_time", scenario.get("cut_limit", 30.0))),
389
+ check_states=bool(scenario.get("check_states", True)),
390
+ oracle=_parse_oracle(d, target),
391
+ plan=_parse_plan(scenario),
392
+ torn=_parse_torn(scenario),
393
+ torn_seed=int(scenario.get("torn_seed", 0)),
394
+ depth=_parse_depth(scenario),
395
+ driver_file=os.path.join(folder, drv["file"]) if drv.get("file") else None,
396
+ driver_builtin=drv.get("builtin"),
397
+ driver_options=drv.get("options", {}) or {},
398
+ cuts=cuts,
399
+ )