Fix tropic emulator discovery and harden upload path handling
What changed, and why it matters
This commit fixes two internal tooling issues in Trezor's firmware test pipeline: it makes the test framework correctly find a new 'Tropic' emulator variant, and it sanitizes the Amazon S3 upload path so a leading or missing trailing slash does not accidentally change where emulator files are stored. There is no direct evidence this is a security vulnerability in the hardware wallet itself, but the S3 path normalization is a hardening measure that could prevent accidental mis-uploads in CI.
Treat as a routine hardening/maintenance commit. No urgent action required. Reviewers may want to verify that the S3 path normalization covers all edge cases (e.g., empty subpath, multiple slashes) and that recursive emulator discovery does not pick up unexpected files.
Security signals we found
S3 upload path normalization (leading-slash removal, trailing-slash enforcement)
Hardened CI artifact upload path handling
Test-only code change; no firmware runtime code modified
No mention of CVE, advisory, researcher credit, or security bug in commit message
Evidence from the diff
The patch modifies GitHub Actions and Python test helpers. In .github/actions/build-core-emu/action.yml, it strips a leading slash from inputs.s3-subpath and ensures a trailing slash is present before concatenating it into an S3 URI. This prevents path-traversal-like mis-uploads (e.g., /subpath becoming model//subpath or model/subpathfilename). In tests/download_emulators.py and tests/emulators.py, it refactors emulator discovery to support a subpath argument and to search recursively (rglob) for emulator binaries, enabling the new Tropic emulator builds to be discovered. The changes are defensive and improve CI/test reliability rather than patching an exploitable runtime bug.
Changed components
.github/actions/build-core-emu/action.ymltests/download_emulators.pytests/emulators.pyInspect captured patch +57 / −33
diff --git a/.github/actions/build-core-emu/action.yml b/.github/actions/build-core-emu/action.yml
index 02da2542..ea87b5f6 100644
--- a/.github/actions/build-core-emu/action.yml
+++ b/.github/actions/build-core-emu/action.yml
@@ -55,7 +55,12 @@ runs:
- name: Upload emulator binaries
if: github.repository == 'trezor/trezor-firmware'
run: |
- aws s3 cp "${{ steps.prepare_binary.outputs.binary_path }}" "s3://data.trezor.io/dev/firmware/releases/emulators-new/${{ inputs.model }}/${{ inputs.s3-subpath }}"
+ SUBPATH="${{ inputs.s3-subpath }}"
+ SUBPATH="${SUBPATH#/}"
+ if [ -n "$SUBPATH" ] && [ "${SUBPATH%/}" = "$SUBPATH" ]; then
+ SUBPATH="${SUBPATH}/"
+ fi
+ aws s3 cp "${{ steps.prepare_binary.outputs.binary_path }}" "s3://data.trezor.io/dev/firmware/releases/emulators-new/${{ inputs.model }}/${SUBPATH}"
shell: sh
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # actions/upload-artifact@v7.0.0
diff --git a/tests/download_emulators.py b/tests/download_emulators.py
index 76bb3658..36fb9029 100755
--- a/tests/download_emulators.py
+++ b/tests/download_emulators.py
@@ -144,36 +144,36 @@ def get_all_releases() -> EmulatorDict:
def get_emulators_for_model(model: str, firmwares: EmulatorDict) -> list[Emulator]:
emulators: list[Emulator] = []
is_tropic_capable = model in TROPIC_CAPABLE_MODELS
+
+ def create_and_check_emulator(
+ version: str, subpath_suffix: str = "", artifact_label: str = "Artifact"
+ ) -> None:
+ subpath = None
+ if subpath_suffix:
+ subpath = f"{model}{subpath_suffix}"
+
+ try:
+ emulator = Emulator(version=version, model=model, subpath=subpath)
+ emulator.check_download_availability()
+ emulators.append(emulator)
+ except KnownMissingArtifactError:
+ # Old artifacts that are known to be unavailable
+ pass
+ except MissingArtifactError as e:
+ click.echo(
+ f"{artifact_label} for model {e.model}, version: {e.version} is unavailable!"
+ )
+
for version, models in firmwares.items():
if model in models:
- try:
- emu = Emulator(version, model)
- emu.check_download_availability()
- emulators.append(emu)
- except KnownMissingArtifactError:
- # Old artifacts that are known to be unavailable
- pass
- except MissingArtifactError as e:
- click.echo(
- f"Artifact for model {e.model}, version: {e.version} is unavailable!"
- )
+ create_and_check_emulator(version=version)
if is_tropic_capable:
- try:
- tropic_emu = Emulator(
- version=version,
- model=model,
- subpath=f"{model}{TROPIC_REMOTE_SUBPATH_SUFFIX}",
- )
- tropic_emu.check_download_availability()
- emulators.append(tropic_emu)
- except KnownMissingArtifactError:
- # Old artifacts that are known to be unavailable
- pass
- except MissingArtifactError as e:
- click.echo(
- f"Tropic artifact for model {e.model}, version: {e.version} is unavailable!"
- )
+ create_and_check_emulator(
+ version=version,
+ subpath_suffix=TROPIC_REMOTE_SUBPATH_SUFFIX,
+ artifact_label="Tropic artifact",
+ )
return emulators
diff --git a/tests/emulators.py b/tests/emulators.py
index df90bcd1..ae62b3ed 100644
--- a/tests/emulators.py
+++ b/tests/emulators.py
@@ -55,22 +55,41 @@ def check_version(tag: str, version_tuple: Tuple[int, int, int]) -> None:
raise RuntimeError(f"Version mismatch: tag {tag} reports version {version}")
-def get_emulator_path(gen: str, model: str, tag: str) -> Path:
- return BINDIR / model / f"trezor-emu-{gen}-{model}-{tag}"
+def get_emulator_path(
+ gen: str,
+ model: str,
+ tag: str,
+ subpath: str | None = None,
+) -> Path:
+ filename = f"trezor-emu-{gen}-{model}-{tag}"
+ base = BINDIR / model
+
+ if subpath is not None:
+ return base / subpath / filename
+
+ direct_path = base / filename
+ if direct_path.exists():
+ return direct_path
+
+ matches = [p for p in base.rglob(filename) if p.is_file()]
+ if matches:
+ return sorted(matches)[0]
+
+ return direct_path
def get_tags() -> dict[str, list[str]]:
- files = [p for p in BINDIR.glob("*/trezor-emu-*") if p.is_file()]
+ files = [p for p in BINDIR.rglob("trezor-emu-*") if p.is_file()]
- result = defaultdict(list)
+ result: dict[str, set[str]] = defaultdict(set)
for f in sorted(files):
try:
# example: "trezor-emu-core-T2T1-v2.0.8" or "trezor-emu-core-T2T1-v2.0.8-46ab42fw"
_, _, _, model, tag = f.name.split("-", maxsplit=4)
- result[model].append(tag)
+ result[model].add(tag)
except ValueError:
pass
- return result
+ return {model: sorted(tags) for model, tags in result.items()}
ALL_TAGS = get_tags()
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.