Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ past roughly six lines it belongs in the PR the entry links.
handler thunk before following the missing edge. Real and generated regression fixtures are bundled.
*Measured against `bfe39ec`:* complete reports bit-identical on 18 bundled non-Delphi x86/x64 PE, ELF and
PE+PDB fixtures. *Reproduced by reviewer on 16.*
- **(arm)** Pass operands through `SmdaInstruction.getMnemonicGroup()` for ARM.
Float-suffixed NEON instructions using `q` registers now classify as vector (`V`); scalar VFP stays float (`F`).
This changes ARM escaped-block signatures, so stored ones need recomputing; recovery and PIC/opcode hashes are
unchanged. Covered by `testArmDisassembler` and the fingerprint corpus's new ARM cases.

### Security

Expand All @@ -129,6 +133,8 @@ past roughly six lines it belongs in the PR the entry links.
- **(labels)** An explicit PDB symbol now takes precedence over PE's synthetic `original_entry_point` label
at the same address.
- **(intel)** Delphi finally continuations extend owning function bodies and can change CFG fingerprints.
- **(arm)** `ESCAPER_DOWNWARD_COMPATIBILITY` moves from `4.4.5` to `5.0.2`, the release that ships the NEON
grouping change, so ARM reports from 5.0.1 and earlier compare as stale.

## [v5.0.1] - 2026-10-20

Expand Down
7 changes: 4 additions & 3 deletions src/smda/SmdaConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ class SmdaConfig:
# Bump this whenever any architecture's InstructionEscaper changes its
# output (mnemonic groups or escaped operands). Downstream indexes such as
# MCRIT treat reports whose smda_version is below this value as stale.
# Last output change: 4.4.5 (segment-qualified memory operands, AVX-512
# registers, six mnemonic groups).
ESCAPER_DOWNWARD_COMPATIBILITY = "4.4.5"
# The value names the release that ships the change, so it leads the
# package version until that release is cut.
# Last output change: 5.0.2 (operand-aware ARM NEON mnemonic groups).
ESCAPER_DOWNWARD_COMPATIBILITY = "5.0.2"
CONFIG_FILE_PATH = str(os.path.abspath(__file__))
PROJECT_ROOT = str(os.path.abspath(os.sep.join([CONFIG_FILE_PATH, "..", "..", ".."])))

Expand Down
3 changes: 2 additions & 1 deletion src/smda/common/SmdaInstruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from capstone.x86 import X86_OP_IMM, X86_OP_MEM

from smda.aarch64.AArch64InstructionEscaper import AArch64InstructionEscaper
from smda.arm.ArmInstructionEscaper import ArmInstructionEscaper
from smda.arm.definitions import IT_MNEMONICS
from smda.intel.IntelInstructionEscaper import IntelInstructionEscaper

Expand Down Expand Up @@ -224,7 +225,7 @@ def _decodeInItBlock(self, capstone):

def getMnemonicGroup(self, escaper):
if escaper:
if escaper is AArch64InstructionEscaper:
if escaper in (AArch64InstructionEscaper, ArmInstructionEscaper):
return escaper.escapeMnemonicForInstruction(self)
return escaper.escapeMnemonic(self.mnemonic)
return self.bytes
Expand Down
5 changes: 5 additions & 0 deletions tests/testArmDisassembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
split_mnemonic,
t32_call_target,
)
from smda.common.SmdaInstruction import SmdaInstruction
from smda.common.SmdaReport import SmdaReport
from smda.Disassembler import Disassembler
from smda.SmdaConfig import SmdaConfig
Expand Down Expand Up @@ -742,6 +743,10 @@ def test_mnemonic_groups(self):
for mnemonic, expected in cases.items():
with self.subTest(mnemonic=mnemonic):
self.assertEqual(ArmInstructionEscaper.escapeMnemonic(mnemonic), expected)
neon_q = SmdaInstruction([0, "000d02f2", "vadd.f32", "q0, q1, q2"])
vfp_s = SmdaInstruction([0, "000a30ee", "vadd.f32", "s0, s0, s0"])
self.assertEqual(neon_q.getMnemonicGroup(ArmInstructionEscaper), "V")
self.assertEqual(vfp_s.getMnemonicGroup(ArmInstructionEscaper), "F")

def test_operand_escaping(self):
cases = [
Expand Down
18 changes: 13 additions & 5 deletions tests/testEscaperFingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
SmdaConfig.ESCAPER_DOWNWARD_COMPATIBILITY to decide whether stored hashes are
stale. That marker did not move when 4.4.5 changed Intel escaping, so this
test hashes a committed corpus of escaped representations. If the digest
moves, bump ESCAPER_DOWNWARD_COMPATIBILITY to the current SmdaConfig.VERSION
in the same change.
moves, set ESCAPER_DOWNWARD_COMPATIBILITY to the release that will ship the
change, the one after the current SmdaConfig.VERSION, in the same change.
"""

import hashlib
Expand All @@ -15,14 +15,16 @@
logging.disable(logging.CRITICAL)

from smda.aarch64.AArch64InstructionEscaper import AArch64InstructionEscaper # noqa: E402
from smda.arm.ArmInstructionEscaper import ArmInstructionEscaper # noqa: E402
from smda.cil.CilInstructionEscaper import CilInstructionEscaper # noqa: E402
from smda.common.SmdaInstruction import SmdaInstruction # noqa: E402
from smda.dalvik.DalvikInstructionEscaper import DalvikInstructionEscaper # noqa: E402
from smda.intel.IntelInstructionEscaper import IntelInstructionEscaper # noqa: E402
from smda.SmdaConfig import SmdaConfig # noqa: E402

# SHA-256 of the canonical escaped lines below. Update together with the
# compatibility marker when any InstructionEscaper changes its output.
ESCAPER_OUTPUT_FINGERPRINT = "3a0a318f568d282734a8879898d564b91914b25105f122c05f4efd657fca4233"
ESCAPER_OUTPUT_FINGERPRINT = "d6a4a33fe9c17f118cdb69d39207e76c2d1a89ddd47491f530f982f39b1f1930"

# (escaper, offset, bytes, mnemonic, operands) — bytes are unused; the
# fingerprint is mnemonic-group + escaped operands, the same pair MCRIT's
Expand Down Expand Up @@ -55,6 +57,8 @@
(AArch64InstructionEscaper, (0, "", "ldr", "x0, [x1, #0x10]")),
(AArch64InstructionEscaper, (0, "", "bl", "#0x400800")),
(AArch64InstructionEscaper, (0, "", "cbz", "w0, #0x14")),
(ArmInstructionEscaper, (0, "000d02f2", "vadd.f32", "q0, q1, q2")),
(ArmInstructionEscaper, (0, "000a30ee", "vadd.f32", "s0, s0, s0")),
(CilInstructionEscaper, (0, "", "ldarg.0", "")),
(CilInstructionEscaper, (0, "", "call", "0x06000001")),
(CilInstructionEscaper, (0, "", "ldstr", "0x70000001")),
Expand Down Expand Up @@ -86,10 +90,14 @@ def test_escaped_representation_fingerprint_is_stable(self):
self.assertEqual(
actual,
ESCAPER_OUTPUT_FINGERPRINT,
"escaper output changed; bump SmdaConfig.ESCAPER_DOWNWARD_COMPATIBILITY "
f"to the current package version and set ESCAPER_OUTPUT_FINGERPRINT to {actual}",
"escaper output changed; set SmdaConfig.ESCAPER_DOWNWARD_COMPATIBILITY "
f"to the next release and ESCAPER_OUTPUT_FINGERPRINT to {actual}",
)

def test_arm_group_change_has_a_compatibility_marker(self):
# 5.0.1 shipped the old NEON grouping; MCRIT selects stale reports with a strict `<`.
self.assertGreaterEqual(tuple(map(int, SmdaConfig.ESCAPER_DOWNWARD_COMPATIBILITY.split("."))), (5, 0, 2))

def test_the_4_4_5_intel_cases_in_the_corpus_escape_as_ptr_not_const(self):
for operands in (
"qword ptr gs:[0x60]",
Expand Down
13 changes: 8 additions & 5 deletions tests/testPackageMetadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,14 @@ def test_a_candidate_orders_below_the_release_it_is_a_candidate_for(self):
def test_the_escaper_compatibility_marker_is_a_three_part_release(self):
self.assertRegex(SmdaConfig.ESCAPER_DOWNWARD_COMPATIBILITY, r"^\d+\.\d+\.\d+$")

def test_the_escaper_compatibility_marker_is_not_newer_than_the_package(self):
self.assertLessEqual(
_version_tuple(SmdaConfig.ESCAPER_DOWNWARD_COMPATIBILITY),
_version_tuple(SmdaConfig.VERSION),
)
def test_the_escaper_compatibility_marker_is_at_most_the_next_release(self):
# An escaper output change sets the marker to the release that will ship it, so
# until that release is cut the marker leads the package by one step. A marker
# further ahead would keep marking the reports of later releases stale.
(major, minor, patch), _ = _version_tuple(SmdaConfig.VERSION)
marker, _ = _version_tuple(SmdaConfig.ESCAPER_DOWNWARD_COMPATIBILITY)
next_releases = {(major, minor, patch + 1), (major, minor + 1, 0), (major + 1, 0, 0)}
self.assertTrue(marker <= (major, minor, patch) or marker in next_releases, marker)

def test_the_escaper_compatibility_marker_covers_the_4_4_5_output_change(self):
# 4.4.5 changed Intel escaped operands (segment-qualified memory, AVX-512)
Expand Down
Loading