tan-cli 0.7.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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Pure facts about a Flow D write, for the envelope (tan-cli#1321, #1318).
|
|
3
|
+
|
|
4
|
+
Two halves, both IO-free:
|
|
5
|
+
|
|
6
|
+
**What a J-Link transcript says (tan-cli#1321).** `verifybin` compares the file
|
|
7
|
+
against J-Link's flash CACHE, not the chip, so "verified" overstated what was
|
|
8
|
+
proven (alp-sdk#2233: on this bench only a fresh-session read after a cold cycle
|
|
9
|
+
proves a write). The envelope reports `verification: "cache-verified"`, the SW-DP
|
|
10
|
+
ID the transcript actually carried, and every reset-failure marker in it
|
|
11
|
+
(tan-cli#522: `Failed to halt CPU` was already caught; `Reset: Failed` and
|
|
12
|
+
`CPU may have not been reset` are the same condition worded by other J-Link
|
|
13
|
+
builds).
|
|
14
|
+
|
|
15
|
+
**What a write would do (tan-cli#1318).** Every write as `{address, size,
|
|
16
|
+
sectorSpan}` with 16 KiB sectors -- the AEN MRAM loader rewrites whole sectors
|
|
17
|
+
and fills the remainder with 0xFF, so a write's footprint is wider than its
|
|
18
|
+
size, and the operator needs it before arming.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
import re
|
|
24
|
+
from collections.abc import Sequence
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from tan.core.dp_id import _dp_id_value
|
|
28
|
+
from tan.core.flash_plan import FlashPlanError, commander_path, validate_commander_path
|
|
29
|
+
|
|
30
|
+
#: The MRAM loader's erase/rewrite granule on the AEN parts (tan-cli#1318).
|
|
31
|
+
SECTOR_BYTES = 16 * 1024
|
|
32
|
+
|
|
33
|
+
#: What `verifybin` proves, in the one spelling the envelope and message use.
|
|
34
|
+
VERIFICATION_CACHE = "cache-verified"
|
|
35
|
+
VERIFICATION_READBACK = "readback-verified"
|
|
36
|
+
#: Stated once, beside the field, so a consumer is not left reading
|
|
37
|
+
#: "verified" as "the chip holds these bytes".
|
|
38
|
+
VERIFICATION_NOTE = (
|
|
39
|
+
"verifybin compares the image against J-Link's flash cache, not the chip; a "
|
|
40
|
+
"fresh-session read after a cold power cycle is what proves a write "
|
|
41
|
+
"(alp-sdk#2233). --readback adds a fresh-session read-back + sha256 compare."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
#: What a matching read-back proves: a FRESH J-Link session read the same bytes. Still
|
|
45
|
+
#: not a cold-cycle proof (alp-sdk#2233).
|
|
46
|
+
VERIFICATION_READBACK_NOTE = (
|
|
47
|
+
"a fresh J-Link session read the written regions back and their sha256 matches the "
|
|
48
|
+
"source files; this is stronger than the flash cache but is not a cold-power-cycle "
|
|
49
|
+
"proof (alp-sdk#2233)."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: J-Link Commander phrasing that means the PIN reset (`RSetType 2` / `r` / `g`)
|
|
53
|
+
#: did not land. Substring-matched against the captured transcript; the exit
|
|
54
|
+
#: code cannot tell these runs apart (JLinkExe still exits 0).
|
|
55
|
+
RESET_FAILURE_MARKERS = (
|
|
56
|
+
"Failed to halt CPU",
|
|
57
|
+
"CPU is not halted",
|
|
58
|
+
"Reset: Failed",
|
|
59
|
+
"CPU may have not been reset",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def reset_failures(transcript: str) -> tuple[str, ...]:
|
|
64
|
+
"""Every reset-failure marker present in `transcript`, in marker order."""
|
|
65
|
+
return tuple(m for m in RESET_FAILURE_MARKERS if m in transcript)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
#: Core-debug DHCSR (0xE000EDF0) bits (tan-cli#1453). S_RESET_ST and S_RETIRE_ST are
|
|
69
|
+
#: sticky but CLEAR ON READ, and J-Link reads DHCSR itself (a failed halt, the probe's
|
|
70
|
+
#: own `connect`) before tan does -- so only S_RESET_ST (a reset happened since the last
|
|
71
|
+
#: read, with no lockup) counts as proof that THIS reset booted. S_SLEEP / S_RETIRE_ST
|
|
72
|
+
#: alone are also what the OLD image idling looks like, so they prove only that a core is
|
|
73
|
+
#: running, never that the reset took.
|
|
74
|
+
DHCSR_ADDRESS = "0xE000EDF0"
|
|
75
|
+
DHCSR_S_HALT = 1 << 17
|
|
76
|
+
DHCSR_S_SLEEP = 1 << 18
|
|
77
|
+
DHCSR_S_LOCKUP = 1 << 19
|
|
78
|
+
DHCSR_S_RETIRE_ST = 1 << 24
|
|
79
|
+
DHCSR_S_RESET_ST = 1 << 25
|
|
80
|
+
_DHCSR_RAN = DHCSR_S_SLEEP | DHCSR_S_RETIRE_ST | DHCSR_S_RESET_ST
|
|
81
|
+
|
|
82
|
+
#: Probe-transcript phrasing that means the probe session itself reset or tried to halt
|
|
83
|
+
#: the core, which makes its DHCSR read evidence of that session, not of the flash's reset.
|
|
84
|
+
PROBE_TROUBLE_MARKERS = ("Reset:",)
|
|
85
|
+
|
|
86
|
+
#: DWT_PCSR, the program-counter sample register: a NON-halting witness of where a RUNNING
|
|
87
|
+
#: core is executing (tan-cli#1453 review). DHCSR's S_RESET_ST is cleared by J-Link's own
|
|
88
|
+
#: reads and will usually be gone, so this is the second witness. It needs the DWT unit
|
|
89
|
+
#: present (Cortex-M55 has it) and readable while the core runs; a halted or sleeping core
|
|
90
|
+
#: returns 0xFFFFFFFF, which is no sample at all.
|
|
91
|
+
DWT_PCSR_ADDRESS = "0xE000101C"
|
|
92
|
+
PCSR_NO_SAMPLE = 0xFFFFFFFF
|
|
93
|
+
#: 0x00000000 is not a sample either (a gated or unpowered DWT reads as zero).
|
|
94
|
+
PCSR_NO_SAMPLES = (PCSR_NO_SAMPLE, 0x00000000)
|
|
95
|
+
PCSR_SAMPLES = 3
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def nohalt_probe_script(jlink_script: str, extra_reads: Sequence[str] = ()) -> str:
|
|
99
|
+
"""A fresh, read-only, NON-halting session: the write's preamble up to and including
|
|
100
|
+
`connect`, one `mem32` of DHCSR, the PCSR samples, then `extra_reads` (the read-only uptime
|
|
101
|
+
reads of tan-cli#1539), `exit` (tan-cli#1453). Raises `ValueError` without a `connect` line."""
|
|
102
|
+
out: list[str] = []
|
|
103
|
+
for line in jlink_script.splitlines():
|
|
104
|
+
out.append(line)
|
|
105
|
+
if line.strip().lower() == "connect":
|
|
106
|
+
break
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError("the write script has no `connect` line to build a probe from")
|
|
109
|
+
out.append(f"mem32 {DHCSR_ADDRESS} 1")
|
|
110
|
+
for _ in range(PCSR_SAMPLES):
|
|
111
|
+
out += [f"mem32 {DWT_PCSR_ADDRESS} 1", "Sleep 5"]
|
|
112
|
+
out += extra_reads
|
|
113
|
+
out.append("exit")
|
|
114
|
+
return "\n".join(out) + "\n"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def dhcsr_in(transcript: str) -> int | None:
|
|
118
|
+
"""The DHCSR word a `mem32 0xE000EDF0 1` printed (`E000EDF0 = 03050001`), or `None`."""
|
|
119
|
+
match = re.search(r"E000EDF0\s*=\s*([0-9A-Fa-f]{8})", transcript)
|
|
120
|
+
return int(match.group(1), 16) if match else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def dhcsr_confirms_reset(value: int | None) -> bool:
|
|
124
|
+
"""True only when `value` shows a reset since the last read (S_RESET_ST) on a core
|
|
125
|
+
that is neither halted nor locked up."""
|
|
126
|
+
return (
|
|
127
|
+
value is not None and bool(value & DHCSR_S_RESET_ST)
|
|
128
|
+
and not value & (DHCSR_S_HALT | DHCSR_S_LOCKUP)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def dhcsr_core_running(value: int | None) -> bool:
|
|
133
|
+
"""True when `value` shows a running core (retired an instruction, slept, or reset)
|
|
134
|
+
that is not halted or locked up. This does NOT say the reset took."""
|
|
135
|
+
return (
|
|
136
|
+
value is not None and bool(value & _DHCSR_RAN)
|
|
137
|
+
and not value & (DHCSR_S_HALT | DHCSR_S_LOCKUP)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def pcsr_samples(transcript: str) -> list[int]:
|
|
142
|
+
"""Every PC sample a `mem32 0xE000101C 1` printed, in order (0xFFFFFFFF included)."""
|
|
143
|
+
return [int(m, 16) for m in re.findall(r"E000101C\s*=\s*([0-9A-Fa-f]{8})", transcript)]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def pcsr_in_ranges(samples: Sequence[int], ranges: Sequence[tuple[int, int]]) -> bool:
|
|
147
|
+
"""True when at least one real sample exists and EVERY real sample lies inside
|
|
148
|
+
`ranges` (`[start, end)` of the image tan just flashed). 0xFFFFFFFF is no evidence; a
|
|
149
|
+
sample outside the ranges (old image, loader, ROM) vetoes. The old image is not known to
|
|
150
|
+
tan, so an old image linked into the same range is not excluded (tan-cli#1537): a True here
|
|
151
|
+
is "the core runs code from this range", never "the new image runs" or "a reset happened"."""
|
|
152
|
+
real = [x for x in samples if x not in PCSR_NO_SAMPLES]
|
|
153
|
+
return bool(real) and bool(ranges) and all(
|
|
154
|
+
any(lo <= x < hi for lo, hi in ranges) for x in real
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def probe_trouble(transcript: str) -> tuple[str, ...]:
|
|
159
|
+
"""Reset/halt markers in a boot-probe transcript (it must have done neither)."""
|
|
160
|
+
return tuple(m for m in (*PROBE_TROUBLE_MARKERS, *RESET_FAILURE_MARKERS) if m in transcript)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def dpidr_in(transcript: str) -> str | None:
|
|
164
|
+
"""The SW-DP ID the transcript reports (`Found SW-DP with ID 0x...` /
|
|
165
|
+
`DPIDR: 0x...`), verbatim, or `None` when it names none."""
|
|
166
|
+
return _dp_id_value(transcript)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def transcript_tail(transcript: str, lines: int = 40) -> list[str]:
|
|
170
|
+
"""The last `lines` non-empty lines of `transcript`."""
|
|
171
|
+
kept = [line.rstrip() for line in transcript.splitlines() if line.strip()]
|
|
172
|
+
return kept[-lines:]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def sector_span(address: int, size: int, sector: int = SECTOR_BYTES) -> dict[str, Any]:
|
|
176
|
+
"""The sectors a write of `size` bytes at `address` touches: the first
|
|
177
|
+
sector's base address, the last sector's END (exclusive), the count and the
|
|
178
|
+
byte footprint (count * sector). A zero-size write touches nothing."""
|
|
179
|
+
if size <= 0:
|
|
180
|
+
return {"first": hex_addr(address - address % sector), "end": hex_addr(address - address % sector),
|
|
181
|
+
"count": 0, "bytes": 0, "sectorBytes": sector}
|
|
182
|
+
first = address - address % sector
|
|
183
|
+
last = (address + size - 1) - (address + size - 1) % sector
|
|
184
|
+
count = (last - first) // sector + 1
|
|
185
|
+
return {
|
|
186
|
+
"first": hex_addr(first),
|
|
187
|
+
"end": hex_addr(last + sector),
|
|
188
|
+
"count": count,
|
|
189
|
+
"bytes": count * sector,
|
|
190
|
+
"sectorBytes": sector,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def hex_addr(value: int) -> str:
|
|
195
|
+
return f"0x{value:08X}"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def planned_write(name: str, address: str, size: int | None, path: str | None) -> dict[str, Any]:
|
|
199
|
+
"""One planned write as the envelope reports it. `size` is `None` when the
|
|
200
|
+
file is not readable yet (the dry-run of a hand-written manifest)."""
|
|
201
|
+
entry: dict[str, Any] = {"name": name, "address": address, "path": path, "size": size}
|
|
202
|
+
if size is not None:
|
|
203
|
+
entry["sectorSpan"] = sector_span(int(address, 16), size)
|
|
204
|
+
return entry
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def sha256_of(path: str) -> str:
|
|
208
|
+
digest = hashlib.sha256()
|
|
209
|
+
with open(path, "rb") as fh:
|
|
210
|
+
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
|
211
|
+
digest.update(chunk)
|
|
212
|
+
return digest.hexdigest()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def reset_tail(jlink_script: str) -> list[str]:
|
|
216
|
+
"""The write script's reset/run tail -- from its first `RSetType` line up to (not
|
|
217
|
+
including) the final `exit` -- so a read-back ends the same way the write did and
|
|
218
|
+
the app is not left halted. `["g"]` when the script has no `RSetType`."""
|
|
219
|
+
lines = [line for line in jlink_script.splitlines() if line.strip()]
|
|
220
|
+
for i, line in enumerate(lines):
|
|
221
|
+
if line.strip().lower().startswith("rsettype"):
|
|
222
|
+
end = len(lines) - 1 if lines[-1].strip().lower() == "exit" else len(lines)
|
|
223
|
+
return lines[i:end]
|
|
224
|
+
return ["g"]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
#: What an in-session read-back proves (tan-cli#1458): the chip's bytes were read back after a
|
|
228
|
+
#: halt, in the write's own session, before the reset -- NOT a fresh session, so J-Link's
|
|
229
|
+
#: flash cache may sit between the read and the cells.
|
|
230
|
+
VERIFICATION_INSESSION = "insession-readback"
|
|
231
|
+
VERIFICATION_INSESSION_NOTE = (
|
|
232
|
+
"the written regions were read back with savebin in the SAME J-Link session as the write, "
|
|
233
|
+
"after a halt and before the PIN reset (a separate session would let the app run in "
|
|
234
|
+
"between); their sha256 matches the source files. J-Link may serve that read from its flash "
|
|
235
|
+
"cache, so this is NOT a fresh-session or cold-power-cycle proof (alp-sdk#2233); "
|
|
236
|
+
"`readback-verified` is reserved for a fresh-session match."
|
|
237
|
+
)
|
|
238
|
+
VERIFICATION_INSESSION_NORESET_NOTE = (
|
|
239
|
+
"the written regions were read back with savebin in the SAME J-Link session as the write, "
|
|
240
|
+
"after a halt; no reset command was sent (--no-reset); their sha256 matches the source "
|
|
241
|
+
"files. J-Link may serve that read from its flash cache, so this is NOT a fresh-session or "
|
|
242
|
+
"cold-power-cycle proof (alp-sdk#2233); `readback-verified` is reserved for a fresh-session "
|
|
243
|
+
"match."
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
#: J-Link Commander phrasing for "no debug access to the target right now" -- a gated
|
|
247
|
+
#: debug domain (the app is in STOP/WFI) as much as a bad cable (tan-cli#1450).
|
|
248
|
+
UNREACHABLE_MARKERS = (
|
|
249
|
+
"Could not read memory",
|
|
250
|
+
"Cannot read memory",
|
|
251
|
+
"Cannot connect to target",
|
|
252
|
+
"Could not connect to target",
|
|
253
|
+
"Connecting to target failed",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def target_unreachable(transcript: str) -> bool:
|
|
258
|
+
"""Whether `transcript` says the target could not be reached for a memory read."""
|
|
259
|
+
return any(m in transcript for m in UNREACHABLE_MARKERS)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def drop_reset_tail(jlink_script: str) -> str:
|
|
263
|
+
"""The write script WITHOUT its reset/run tail (`RSetType` .. final `exit`): connect,
|
|
264
|
+
loadbin, verifybin, `exit`. For `--no-reset` (tan-cli#1445): no reset command is sent. J-Link's
|
|
265
|
+
`exit` may still resume the core (bench, tan-cli#1458), which is why the envelope says so."""
|
|
266
|
+
lines = [line for line in jlink_script.splitlines() if line.strip()]
|
|
267
|
+
for i, line in enumerate(lines):
|
|
268
|
+
if line.strip().lower().startswith("rsettype"):
|
|
269
|
+
return "\n".join([*lines[:i], "exit"]) + "\n"
|
|
270
|
+
return jlink_script
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def combined_script(
|
|
274
|
+
jlink_script: str, regions: Sequence[tuple[str, int, str]], tail: bool = True
|
|
275
|
+
) -> str:
|
|
276
|
+
"""The write session WITH the read-back inside it (tan-cli#1458, bench): everything up to
|
|
277
|
+
the first `RSetType` (connect, loadbin, verifybin), then `h`, one `savebin` per region,
|
|
278
|
+
the write's own reset/run tail, `exit`. There is NO `exit` between the write and the read:
|
|
279
|
+
J-Link's `exit` resumes the core even after `h`, so a separate read-back session let the
|
|
280
|
+
app run on stale state before it connected. `regions` is `(address_hex, size, dest)`.
|
|
281
|
+
`tail=False` (`--no-reset`) ends after the read with `exit` and no reset/run commands; the
|
|
282
|
+
script then needs no `RSetType`. Otherwise raises `ValueError` without a `RSetType` tail to
|
|
283
|
+
keep (nothing to run after the read)."""
|
|
284
|
+
lines = [line for line in jlink_script.splitlines() if line.strip()]
|
|
285
|
+
for i, line in enumerate(lines):
|
|
286
|
+
if line.strip().lower().startswith("rsettype"):
|
|
287
|
+
break
|
|
288
|
+
else:
|
|
289
|
+
if tail:
|
|
290
|
+
raise ValueError("the write script has no reset tail to run after the read-back")
|
|
291
|
+
i = len(lines) - 1 if lines and lines[-1].strip().lower() == "exit" else len(lines)
|
|
292
|
+
tail_lines = reset_tail(jlink_script) if tail else []
|
|
293
|
+
out = [*lines[:i], "h"]
|
|
294
|
+
for address, size, dest in regions:
|
|
295
|
+
validate_commander_path(dest, "the read-back destination path")
|
|
296
|
+
out.append(f"savebin {commander_path(dest)} {address} 0x{size:X}")
|
|
297
|
+
return "\n".join([*out, *tail_lines, "exit"]) + "\n"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def readback_script(
|
|
301
|
+
jlink_script: str,
|
|
302
|
+
regions: Sequence[tuple[str, int, str]],
|
|
303
|
+
) -> str:
|
|
304
|
+
"""The Commander script of a FRESH read-back session: the same preamble the
|
|
305
|
+
write used (everything up to and including `connect`, so the same probe,
|
|
306
|
+
interface, speed and part-number device), then one `savebin <file>, <addr>,
|
|
307
|
+
<size>` per region and `exit`. `regions` is `(address_hex, size, dest_path)`.
|
|
308
|
+
The session ends at `exit` WITHOUT the write's reset/run tail (a raw sector write must
|
|
309
|
+
not reset or run anything, tan-cli#1446; Flow D reads inside its write session instead,
|
|
310
|
+
see [`combined_script`]).
|
|
311
|
+
Raises `ValueError` if `jlink_script` has no `connect` line."""
|
|
312
|
+
out: list[str] = []
|
|
313
|
+
for line in jlink_script.splitlines():
|
|
314
|
+
out.append(line)
|
|
315
|
+
if line.strip().lower() == "connect":
|
|
316
|
+
break
|
|
317
|
+
else:
|
|
318
|
+
raise ValueError("the write script has no `connect` line to build a read-back from")
|
|
319
|
+
for address, size, dest in regions:
|
|
320
|
+
validate_commander_path(dest, "the read-back destination path")
|
|
321
|
+
out.append(f"savebin {commander_path(dest)} {address} 0x{size:X}")
|
|
322
|
+
out.append("exit")
|
|
323
|
+
return "\n".join(out) + "\n"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ── sector overlap (tan-cli#1343 review) ────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
CODE_SECTOR_OVERLAP = "flash.write-sector-overlap"
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class SectorOverlapError(FlashPlanError):
|
|
332
|
+
"""Two writes (or a write and a resident entry that will not be rewritten)
|
|
333
|
+
share a 16 KiB sector. `code` is the registered issue code."""
|
|
334
|
+
|
|
335
|
+
code = CODE_SECTOR_OVERLAP
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _interval(address: int, size: int | None) -> tuple[int, int]:
|
|
339
|
+
"""`[first, end)` of the sectors touched by `size` bytes at `address`; an
|
|
340
|
+
unknown size counts as one sector (the least it can touch)."""
|
|
341
|
+
first = address - address % SECTOR_BYTES
|
|
342
|
+
last = address + max((size or 1), 1) - 1
|
|
343
|
+
return first, last - last % SECTOR_BYTES + SECTOR_BYTES
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def find_overlaps(
|
|
347
|
+
writes: Sequence[dict[str, Any]],
|
|
348
|
+
resident: Sequence[tuple[str, int | None, int | None]] = (),
|
|
349
|
+
) -> list[str]:
|
|
350
|
+
"""Human descriptions of every sector overlap among `writes`
|
|
351
|
+
(`{name, address, size}` dicts) and between a write and a `resident`
|
|
352
|
+
`(name, address, size)` region (entries with no address are skipped: tan
|
|
353
|
+
cannot place what it was not told). The loader rewrites WHOLE sectors, so a
|
|
354
|
+
write whose tail reaches the next write's first sector would erase the
|
|
355
|
+
neighbour's head."""
|
|
356
|
+
spans = [
|
|
357
|
+
(w["name"], *_interval(int(w["address"], 16), w.get("size")))
|
|
358
|
+
for w in writes
|
|
359
|
+
if w.get("address")
|
|
360
|
+
]
|
|
361
|
+
out: list[str] = []
|
|
362
|
+
for i, (a_name, a_lo, a_hi) in enumerate(spans):
|
|
363
|
+
for b_name, b_lo, b_hi in spans[i + 1 :]:
|
|
364
|
+
if a_lo < b_hi and b_lo < a_hi:
|
|
365
|
+
lo, hi = max(a_lo, b_lo), min(a_hi, b_hi)
|
|
366
|
+
out.append(
|
|
367
|
+
f"the {a_name} write and the {b_name} write share the 16 KiB sector(s) "
|
|
368
|
+
f"{hex_addr(lo)}-{hex_addr(hi)}"
|
|
369
|
+
)
|
|
370
|
+
for r_name, r_addr, r_size in resident:
|
|
371
|
+
if r_addr is None:
|
|
372
|
+
continue
|
|
373
|
+
r_lo, r_hi = _interval(r_addr, r_size)
|
|
374
|
+
for w_name, w_lo, w_hi in spans:
|
|
375
|
+
if w_lo < r_hi and r_lo < w_hi:
|
|
376
|
+
out.append(
|
|
377
|
+
f"the {w_name} write's sectors {hex_addr(w_lo)}-{hex_addr(w_hi)} cover the "
|
|
378
|
+
f"resident entry {r_name} at {hex_addr(r_addr)}, and no write here replaces it "
|
|
379
|
+
"(a write starting at exactly that address does)"
|
|
380
|
+
)
|
|
381
|
+
return out
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""tan-cli#1539: a boot witness for Flow D -- the kernel uptime, bounded by the wall clock.
|
|
3
|
+
|
|
4
|
+
After a write whose halt failed, DHCSR `S_RESET_ST` is usually gone and PC samples cannot tell the
|
|
5
|
+
new image from the replaced one (tan-cli#1537). A free-running tick counter can, provided it is
|
|
6
|
+
compared with the time tan itself measured: the probe's last read must show an uptime no larger
|
|
7
|
+
than the monotonic time elapsed since the write session ended (plus a slack for the reset tail
|
|
8
|
+
inside that session). SysTick stops while the core is halted for the write, so an image that was
|
|
9
|
+
already running when the write began resumes with its uptime frozen at the halt; the bound admits it
|
|
10
|
+
only if that frozen uptime was at most the slack, i.e. it booted under ~1 s before the write halted
|
|
11
|
+
it, which the preceding probe/preflight sessions already rule out in practice.
|
|
12
|
+
This proves a RECENT BOOT, not that the SE PIN reset took (reported as `reset: "booted"`).
|
|
13
|
+
|
|
14
|
+
The counter is Zephyr's kernel tick count (`curr_tick`, a `static uint64_t` in `kernel/timeout.c`;
|
|
15
|
+
present in an AEN alp-console ELF at a DTCM address). Its address is resolved from the ELF being
|
|
16
|
+
flashed, never hardcoded, and only an address inside a RAM LOAD segment of the slice core's own
|
|
17
|
+
DTCM / SRAM0 is ever read (never HP ITCM 0x50000000, never anything outside the image). The tick
|
|
18
|
+
rate comes from the same ELF's absolute `CONFIG_SYS_CLOCK_TICKS_PER_SEC` symbol, else the build's
|
|
19
|
+
`.config` beside it. Anything missing, ambiguous or out of range yields no probe, and the caller
|
|
20
|
+
keeps the honest `unconfirmed`. Pure: no IO except `load_uptime_probe` reading two local files."""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Sequence
|
|
27
|
+
|
|
28
|
+
from tan.core.ram_run import ElfImage, RamRunError, parse_elf
|
|
29
|
+
|
|
30
|
+
UPTIME_SYMBOL = "curr_tick"
|
|
31
|
+
TICKS_PER_SEC_SYMBOL = "CONFIG_SYS_CLOCK_TICKS_PER_SEC"
|
|
32
|
+
#: Reads per session, and the pause between them (ms). The tick only advances when the timeout
|
|
33
|
+
#: ISR announces, so the window must span more than one idle wake-up of a typical sleeper.
|
|
34
|
+
UPTIME_READS = 4
|
|
35
|
+
UPTIME_SLEEP_MS = 600
|
|
36
|
+
#: Slack (s) added to the elapsed time: the reset tail runs inside the write session, so the
|
|
37
|
+
#: image boots slightly BEFORE the session's end timestamp. Measured on e1m-aen-evk-02
|
|
38
|
+
#: (alp-console, 2026-10-11): uptime - elapsed = -0.04..-0.05 s, so 1 s is all headroom.
|
|
39
|
+
UPTIME_SLACK_SECONDS = 1.0
|
|
40
|
+
#: The only address windows ever read: the slice core's DTCM (local alias, 1 MiB window) and the
|
|
41
|
+
#: E8's 4 MiB SRAM0. HP ITCM (0x50000000) and every other aperture are out.
|
|
42
|
+
_READABLE = ((0x20000000, 0x20100000), (0x02000000, 0x02400000))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class UptimeProbe:
|
|
47
|
+
address: int
|
|
48
|
+
size: int # 4 or 8 bytes
|
|
49
|
+
ticks_per_sec: int
|
|
50
|
+
symbol: str = UPTIME_SYMBOL
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def words(self) -> int:
|
|
54
|
+
return self.size // 4
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def plan_uptime_probe(elf: ElfImage, config_ticks_per_sec: int | None = None) -> UptimeProbe | None:
|
|
58
|
+
"""The probe for `elf`, or `None` when the symbol is missing, ambiguous, oddly sized, outside
|
|
59
|
+
the readable RAM windows or outside a RAM LOAD segment, or the tick rate is unknown."""
|
|
60
|
+
if UPTIME_SYMBOL in elf.ambiguous or UPTIME_SYMBOL not in elf.symbols:
|
|
61
|
+
return None
|
|
62
|
+
address, size = elf.symbols[UPTIME_SYMBOL]
|
|
63
|
+
if size not in (4, 8) or address % 4:
|
|
64
|
+
return None
|
|
65
|
+
end = address + size
|
|
66
|
+
if not any(lo <= address and end <= hi for lo, hi in _READABLE):
|
|
67
|
+
return None
|
|
68
|
+
if not any(s.vaddr <= address and end <= s.vaddr + s.memsz for s in elf.segments):
|
|
69
|
+
return None
|
|
70
|
+
rate = config_ticks_per_sec
|
|
71
|
+
if TICKS_PER_SEC_SYMBOL in elf.symbols and TICKS_PER_SEC_SYMBOL not in elf.ambiguous:
|
|
72
|
+
rate = elf.symbols[TICKS_PER_SEC_SYMBOL][0]
|
|
73
|
+
if not rate or rate <= 0:
|
|
74
|
+
return None
|
|
75
|
+
return UptimeProbe(address, size, rate)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def config_ticks_per_sec(config_text: str) -> int | None:
|
|
79
|
+
match = re.search(r"^CONFIG_SYS_CLOCK_TICKS_PER_SEC=(\d+)\s*$", config_text, re.MULTILINE)
|
|
80
|
+
return int(match.group(1)) if match else None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_elf(elf_path: str) -> ElfImage | None:
|
|
84
|
+
"""The parsed ELF at `elf_path`, or `None` when it cannot be read or parsed."""
|
|
85
|
+
try:
|
|
86
|
+
with open(elf_path, "rb") as fh:
|
|
87
|
+
return parse_elf(fh.read())
|
|
88
|
+
except (OSError, RamRunError):
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_uptime_probe(elf: ElfImage | None, elf_path: str) -> UptimeProbe | None:
|
|
93
|
+
"""`plan_uptime_probe` for the parsed `elf` (read from `elf_path`, whose directory may hold
|
|
94
|
+
the build's `.config`); `None` when there is no ELF."""
|
|
95
|
+
if elf is None:
|
|
96
|
+
return None
|
|
97
|
+
rate = None
|
|
98
|
+
try:
|
|
99
|
+
with open(os.path.join(os.path.dirname(elf_path), ".config"), encoding="utf-8") as fh:
|
|
100
|
+
rate = config_ticks_per_sec(fh.read())
|
|
101
|
+
except OSError:
|
|
102
|
+
pass
|
|
103
|
+
return plan_uptime_probe(elf, rate)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def uptime_script_lines(probe: UptimeProbe) -> list[str]:
|
|
107
|
+
"""Read-only Commander lines: `UPTIME_READS` non-halting `mem32` reads of the counter."""
|
|
108
|
+
out: list[str] = []
|
|
109
|
+
for i in range(UPTIME_READS):
|
|
110
|
+
out.append(f"mem32 0x{probe.address:08X} {probe.words}")
|
|
111
|
+
if i + 1 < UPTIME_READS:
|
|
112
|
+
out.append(f"Sleep {UPTIME_SLEEP_MS}")
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def uptime_ticks(transcript: str, probe: UptimeProbe) -> list[int]:
|
|
117
|
+
"""Every counter value the probe's `mem32` lines printed, in order (little-endian words)."""
|
|
118
|
+
pattern = rf"(?<![0-9A-Fa-f]){probe.address:08X}\s*=\s*((?:[0-9A-Fa-f]{{8}}\s*){{{probe.words}}})"
|
|
119
|
+
values: list[int] = []
|
|
120
|
+
for match in re.finditer(pattern, transcript, re.IGNORECASE):
|
|
121
|
+
words = [int(w, 16) for w in match.group(1).split()]
|
|
122
|
+
values.append(sum(w << (32 * i) for i, w in enumerate(words)))
|
|
123
|
+
return values
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def uptime_confirms_boot(
|
|
127
|
+
ticks: Sequence[int], ticks_per_sec: int, elapsed_s: float,
|
|
128
|
+
slack_s: float = UPTIME_SLACK_SECONDS,
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""True only for a counter that moved (never going backwards, last > first) and whose last
|
|
131
|
+
value is no more uptime than `elapsed_s` (monotonic seconds since the write session ended)
|
|
132
|
+
plus `slack_s`. A constant counter is a halted or gated core; a larger uptime is an image
|
|
133
|
+
that was running before the write; an all-ones word is an unreadable bus, not a count. The
|
|
134
|
+
64-bit counter is read as two 32-bit words, so a read can tear across a carry: it shows as a
|
|
135
|
+
huge jump or a step backwards and is rejected by the same rules."""
|
|
136
|
+
if len(ticks) < 2 or any(t in (0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF) for t in ticks):
|
|
137
|
+
return False
|
|
138
|
+
if any(b < a for a, b in zip(ticks, ticks[1:])) or ticks[-1] <= ticks[0]:
|
|
139
|
+
return False
|
|
140
|
+
return ticks[-1] / ticks_per_sec <= elapsed_s + slack_s
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def uptime_evidence(
|
|
144
|
+
transcript: str, probe: UptimeProbe, elapsed_s: float
|
|
145
|
+
) -> tuple[dict, dict | None]:
|
|
146
|
+
"""`(bootProbe.uptime evidence, jlink-block update)`; the update is `None` unless the
|
|
147
|
+
counter confirms a boot."""
|
|
148
|
+
ticks = uptime_ticks(transcript, probe)
|
|
149
|
+
evidence = {
|
|
150
|
+
"symbol": probe.symbol, "address": f"0x{probe.address:08X}",
|
|
151
|
+
"ticks": [f"0x{t:X}" for t in ticks], "ticksPerSec": probe.ticks_per_sec,
|
|
152
|
+
"elapsedSinceWriteSeconds": round(elapsed_s, 2),
|
|
153
|
+
}
|
|
154
|
+
if not uptime_confirms_boot(ticks, probe.ticks_per_sec, elapsed_s):
|
|
155
|
+
return evidence, None
|
|
156
|
+
return evidence, {
|
|
157
|
+
"coreRunning": True, "reset": "booted", "resetConfirmedBy": "uptime",
|
|
158
|
+
"witness": f"{probe.symbol} 0x{ticks[0]:X} -> 0x{ticks[-1]:X}, up "
|
|
159
|
+
f"{ticks[-1] / probe.ticks_per_sec:.1f} s, {elapsed_s:.1f} s after the write",
|
|
160
|
+
"resetNote": "the halt after the reset failed, but the kernel tick counter "
|
|
161
|
+
f"({probe.symbol}, read without halting at the flashed ELF's own address) is rising and "
|
|
162
|
+
"shows no more uptime than the time elapsed since the write ended: the image booted "
|
|
163
|
+
"after the write. That proves a recent boot, not that the SE PIN reset took.",
|
|
164
|
+
}
|
tan/core/fs_confine.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The one containment guard every ACTIVE project writer trusts.
|
|
3
|
+
|
|
4
|
+
`tan init` (`tan.core.scaffold.write_files`) and `tan generate`
|
|
5
|
+
(`tan.commands.generate_cmd`, its default -- non-`--output` -- targets) both
|
|
6
|
+
join a caller-controlled relative path onto a project root and write there.
|
|
7
|
+
Neither used to check the RESOLVED target after following symlinks, so a
|
|
8
|
+
pre-existing directory symlink under the project (`<project>/src` ->
|
|
9
|
+
somewhere else) or a symlinked existing leaf silently redirected the write
|
|
10
|
+
outside the project while the envelope still reported the logical in-project
|
|
11
|
+
path as written (tan-cli#325).
|
|
12
|
+
|
|
13
|
+
`confine_to_build_root` (`tan.commands.build.materialise`) is the same
|
|
14
|
+
question for a DIFFERENT root (a build root, reused by `tan clean`) with its
|
|
15
|
+
own coded error (`build.path-escape`) and its own message shape; this module
|
|
16
|
+
is `resolve_confined` for the other two writers, not a third hand-rolled copy
|
|
17
|
+
of the same predicate (tan-cli#322 is exactly this drift: bootstrap and doctor
|
|
18
|
+
each grew their own resolver).
|
|
19
|
+
|
|
20
|
+
`Path.is_relative_to()` on TWO resolved paths, not a string prefix test:
|
|
21
|
+
resolving both sides is what makes a symlinked PROJECT ROOT itself still work
|
|
22
|
+
(`is_relative_to` only needs the root to be a prefix of the target's own
|
|
23
|
+
resolved components, so a project opened through a symlink still confines
|
|
24
|
+
correctly -- the escape this guards against is a write LANDING outside the
|
|
25
|
+
root, not the root being reached through one), and `is_relative_to` is
|
|
26
|
+
correct on Windows where a naive `str(target).startswith(str(root))` is not:
|
|
27
|
+
path comparison there folds case (`C:\\Foo` vs `c:\\foo`) and a bare
|
|
28
|
+
`startswith` would also wrongly accept a sibling directory whose name happens
|
|
29
|
+
to share the root's as a prefix (`C:\\proj` vs `C:\\project2`).
|
|
30
|
+
|
|
31
|
+
That case-folding is a `PureWindowsPath` property, NOT a filesystem one, and
|
|
32
|
+
those two came apart on macOS (tan-cli#587). `PurePosixPath.is_relative_to`
|
|
33
|
+
compares case-SENSITIVELY, and `Path.resolve()` does not canonicalise case on
|
|
34
|
+
a POSIX flavour to hide the difference -- `posixpath.realpath` walks the path
|
|
35
|
+
with `os.lstat`/`os.readlink` only and never asks the filesystem for a
|
|
36
|
+
component's true on-disk name, so `<tmp>/PROJECT` stays `<tmp>/PROJECT` even
|
|
37
|
+
where it names the very directory created as `<tmp>/Project`. On APFS (the
|
|
38
|
+
macOS default, case-INSENSITIVE and case-preserving) that made the guard
|
|
39
|
+
refuse a target genuinely inside the root. The failure direction was
|
|
40
|
+
over-restriction, never a bypass, but a `PathEscapeError` naming a path the
|
|
41
|
+
user can plainly see inside their project is a refusal that cannot be
|
|
42
|
+
trusted, and this module exists to be trusted.
|
|
43
|
+
|
|
44
|
+
The fallback below is INODE IDENTITY (`os.path.samestat`), not case folding.
|
|
45
|
+
Folding would be wrong on a case-sensitive volume, where `<tmp>/Project` and
|
|
46
|
+
`<tmp>/PROJECT` are two different directories and accepting the second as
|
|
47
|
+
inside the first is exactly the escape this guards against; asking the
|
|
48
|
+
filesystem whether the two names reach the same directory is right on both
|
|
49
|
+
kinds of volume, and needs no guess about which kind is underfoot.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import os
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PathEscapeError(Exception):
|
|
59
|
+
"""`target` resolved outside `root` after symlink resolution. Carries both
|
|
60
|
+
resolved paths so a caller can phrase its own coded issue message rather
|
|
61
|
+
than parsing this one."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, root: Path, target: Path) -> None:
|
|
64
|
+
super().__init__(f"'{target}' resolves outside '{root}'")
|
|
65
|
+
self.root = root
|
|
66
|
+
self.target = target
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _reaches_the_same_root(resolved_root: Path, resolved_target: Path) -> bool:
|
|
70
|
+
"""True when `resolved_target`'s first `len(resolved_root.parts)` components
|
|
71
|
+
name the SAME directory as `resolved_root`, spelled differently.
|
|
72
|
+
|
|
73
|
+
Only the prefix at the root's own depth is compared, and only by
|
|
74
|
+
`os.stat` identity: if that prefix IS the root directory, everything after
|
|
75
|
+
it lands inside the root by construction -- `resolved_target` has already
|
|
76
|
+
had every symlink followed by `Path.resolve()`, so the remaining
|
|
77
|
+
components are literal, not another indirection waiting to redirect the
|
|
78
|
+
write.
|
|
79
|
+
|
|
80
|
+
A shorter target than the root cannot be inside it, and an `OSError` (the
|
|
81
|
+
prefix does not exist, or is unreadable) answers the question `False`:
|
|
82
|
+
this runs only after `is_relative_to` already said no, so `False` simply
|
|
83
|
+
keeps the pre-existing refusal.
|
|
84
|
+
|
|
85
|
+
Not restricted to case: a bind mount or any other alias that makes two
|
|
86
|
+
spellings reach one directory answers `True` here, which is the same
|
|
87
|
+
answer for the same reason -- the write lands inside the root either way,
|
|
88
|
+
and "the write LANDS outside the root" is the escape being guarded.
|
|
89
|
+
"""
|
|
90
|
+
root_parts = resolved_root.parts
|
|
91
|
+
target_parts = resolved_target.parts
|
|
92
|
+
if len(target_parts) < len(root_parts):
|
|
93
|
+
return False
|
|
94
|
+
prefix = Path(*target_parts[: len(root_parts)])
|
|
95
|
+
try:
|
|
96
|
+
return os.path.samestat(os.stat(prefix), os.stat(resolved_root))
|
|
97
|
+
except OSError:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def resolve_confined(root: Path, target: Path) -> Path:
|
|
102
|
+
"""Resolve `target` and require it stays inside the resolved `root`.
|
|
103
|
+
|
|
104
|
+
Returns the resolved target. Raises `PathEscapeError` when it does not --
|
|
105
|
+
including when a symlinked (or junctioned) PARENT directory of `target`
|
|
106
|
+
points outside `root`, and when `target` itself is a symlink to a file
|
|
107
|
+
outside `root`; `Path.resolve()` follows both. `target` need not exist:
|
|
108
|
+
`resolve(strict=False)` (the default) resolves every symlink it finds and
|
|
109
|
+
appends any remaining, not-yet-created components literally, so a `new`
|
|
110
|
+
file under a symlinked parent is caught exactly like an existing one.
|
|
111
|
+
|
|
112
|
+
Callers should also catch `OSError`/`ValueError` around this call: a path
|
|
113
|
+
shape the host rejects outright (a Windows device-namespace path, one
|
|
114
|
+
embedding a NUL) raises there, not here, and is a refusal either way.
|
|
115
|
+
|
|
116
|
+
The returned path keeps the CALLER's spelling of the components it was
|
|
117
|
+
given (`<tmp>/PROJECT/board.yaml`, not `<tmp>/Project/board.yaml`) on the
|
|
118
|
+
identity path: on a case-insensitive volume both spellings open the same
|
|
119
|
+
file, and re-spelling a caller's path from the on-disk casing would need
|
|
120
|
+
a `readdir` per component that `Path.resolve()` never performs. Compare
|
|
121
|
+
two returned paths with `os.path.samefile`, not `==`, if a test needs to
|
|
122
|
+
tie one back to a differently-spelled root (tan-cli#587).
|
|
123
|
+
"""
|
|
124
|
+
resolved_root = root.resolve()
|
|
125
|
+
resolved_target = target.resolve()
|
|
126
|
+
if not resolved_target.is_relative_to(resolved_root) and not _reaches_the_same_root(
|
|
127
|
+
resolved_root, resolved_target
|
|
128
|
+
):
|
|
129
|
+
raise PathEscapeError(resolved_root, resolved_target)
|
|
130
|
+
return resolved_target
|