diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d520228..820ed6d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/src/smda/SmdaConfig.py b/src/smda/SmdaConfig.py index e02abccb..c83b1813 100644 --- a/src/smda/SmdaConfig.py +++ b/src/smda/SmdaConfig.py @@ -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, "..", "..", ".."]))) diff --git a/src/smda/common/SmdaInstruction.py b/src/smda/common/SmdaInstruction.py index ed5480ce..efa12e8d 100644 --- a/src/smda/common/SmdaInstruction.py +++ b/src/smda/common/SmdaInstruction.py @@ -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 @@ -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 diff --git a/tests/testArmDisassembler.py b/tests/testArmDisassembler.py index bedf2e83..573cd3dd 100644 --- a/tests/testArmDisassembler.py +++ b/tests/testArmDisassembler.py @@ -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 @@ -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 = [ diff --git a/tests/testEscaperFingerprint.py b/tests/testEscaperFingerprint.py index 178579e1..655ebf13 100644 --- a/tests/testEscaperFingerprint.py +++ b/tests/testEscaperFingerprint.py @@ -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 @@ -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 @@ -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")), @@ -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]", diff --git a/tests/testPackageMetadata.py b/tests/testPackageMetadata.py index 3b56d2ea..45dff255 100644 --- a/tests/testPackageMetadata.py +++ b/tests/testPackageMetadata.py @@ -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)