From 0a2a456dd08fae6e34c6dd36d501176ebf197714 Mon Sep 17 00:00:00 2001 From: r0ny123 <49360849+r0ny123@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:08:36 +0530 Subject: [PATCH 1/3] fix(arm): pass instruction operands when escaping ARM mnemonic groups SmdaInstruction.getMnemonicGroup() only dispatched to escapeMnemonicForInstruction() for AArch64InstructionEscaper and fell back to escapeMnemonic(self.mnemonic) without operands for ArmInstructionEscaper. That left ArmInstructionEscaper.escapeMnemonicForInstruction() unreachable and classified 128-bit NEON vector instructions with float type suffixes (such as vadd.f32 q0, q1, q2) as "F" instead of "V". --- CHANGELOG.md | 5 +++++ src/smda/common/SmdaInstruction.py | 3 ++- tests/testArmDisassembler.py | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 517465db..7412506a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,11 @@ past roughly six lines it belongs in the PR the entry links. ### Removed ### Fixed +- **(arm)** Pass the instruction's operands when escaping ARM mnemonic groups through `SmdaInstruction.getMnemonicGroup()`. + `getMnemonicGroup()` only dispatched to `escapeMnemonicForInstruction()` for `AArch64InstructionEscaper` and fell back to + `escapeMnemonic(self.mnemonic)` without operands for `ArmInstructionEscaper`, so 128-bit NEON vector instructions with + float type suffixes (`vadd.f32 q0, q1, q2`) were classified as `"F"` instead of `"V"`. *Reproduced in + `tests/testArmDisassembler.py`.* No cost: function and block recovery and `pic_hash`/`opc_hash` are unchanged. ### Security 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 = [ From 96b13bdf239d6fe0d9a762c524b5ef333fd100d1 Mon Sep 17 00:00:00 2001 From: r0ny123 <49360849+r0ny123@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:36:30 +0530 Subject: [PATCH 2/3] fix(arm): track operand-aware mnemonic compatibility Cover NEON and scalar VFP groups in the fingerprint corpus and advance the compatibility marker to the current package version. Document that affected ARM signatures, including 5.0.1 reports, need recomputing. PIC/opcode hashes and CFG recovery are unchanged. Validation: ARM and fingerprint tests; lint; formatting; type checks. --- CHANGELOG.md | 10 +++++----- src/smda/SmdaConfig.py | 5 ++--- tests/testEscaperFingerprint.py | 9 ++++++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7412506a..5168f968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,11 +79,11 @@ past roughly six lines it belongs in the PR the entry links. ### Removed ### Fixed -- **(arm)** Pass the instruction's operands when escaping ARM mnemonic groups through `SmdaInstruction.getMnemonicGroup()`. - `getMnemonicGroup()` only dispatched to `escapeMnemonicForInstruction()` for `AArch64InstructionEscaper` and fell back to - `escapeMnemonic(self.mnemonic)` without operands for `ArmInstructionEscaper`, so 128-bit NEON vector instructions with - float type suffixes (`vadd.f32 q0, q1, q2`) were classified as `"F"` instead of `"V"`. *Reproduced in - `tests/testArmDisassembler.py`.* No cost: function and block recovery and `pic_hash`/`opc_hash` are unchanged. +- **(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 escaped-block signatures: `ESCAPER_DOWNWARD_COMPATIBILITY` is now `5.0.1`, and stored ARM + escaped-block signatures need recomputing, including reports already labelled `5.0.1`. Recovery and PIC/opcode hashes are unchanged. + Covered by `testArmDisassembler` and the fingerprint corpus's new ARM cases. ### Security diff --git a/src/smda/SmdaConfig.py b/src/smda/SmdaConfig.py index d4425f3b..667cb399 100644 --- a/src/smda/SmdaConfig.py +++ b/src/smda/SmdaConfig.py @@ -9,9 +9,8 @@ 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" + # Last output change: 5.0.1 (operand-aware ARM NEON mnemonic groups). + ESCAPER_DOWNWARD_COMPATIBILITY = "5.0.1" CONFIG_FILE_PATH = str(os.path.abspath(__file__)) PROJECT_ROOT = str(os.path.abspath(os.sep.join([CONFIG_FILE_PATH, "..", "..", ".."]))) diff --git a/tests/testEscaperFingerprint.py b/tests/testEscaperFingerprint.py index 178579e1..cd77bbea 100644 --- a/tests/testEscaperFingerprint.py +++ b/tests/testEscaperFingerprint.py @@ -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")), @@ -90,6 +94,9 @@ def test_escaped_representation_fingerprint_is_stable(self): f"to the current package version and set ESCAPER_OUTPUT_FINGERPRINT to {actual}", ) + def test_arm_group_change_has_a_compatibility_marker(self): + self.assertGreaterEqual(tuple(map(int, SmdaConfig.ESCAPER_DOWNWARD_COMPATIBILITY.split("."))), (5, 0, 1)) + def test_the_4_4_5_intel_cases_in_the_corpus_escape_as_ptr_not_const(self): for operands in ( "qword ptr gs:[0x60]", From 0de97972e0df9e15c55ce8d37aacd68b2d33af58 Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:05:07 +0000 Subject: [PATCH 3/3] fix(arm): set the escaper marker to the release that ships the NEON change --- CHANGELOG.md | 7 ++++--- src/smda/SmdaConfig.py | 6 ++++-- tests/testEscaperFingerprint.py | 11 ++++++----- tests/testPackageMetadata.py | 13 ++++++++----- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5168f968..c7c80f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,13 +81,14 @@ past roughly six lines it belongs in the PR the entry links. ### Fixed - **(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 escaped-block signatures: `ESCAPER_DOWNWARD_COMPATIBILITY` is now `5.0.1`, and stored ARM - escaped-block signatures need recomputing, including reports already labelled `5.0.1`. Recovery and PIC/opcode hashes are unchanged. - Covered by `testArmDisassembler` and the fingerprint corpus's new ARM cases. + 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 ### Compatibility +- **(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 667cb399..4ff4558f 100644 --- a/src/smda/SmdaConfig.py +++ b/src/smda/SmdaConfig.py @@ -9,8 +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: 5.0.1 (operand-aware ARM NEON mnemonic groups). - ESCAPER_DOWNWARD_COMPATIBILITY = "5.0.1" + # 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/tests/testEscaperFingerprint.py b/tests/testEscaperFingerprint.py index cd77bbea..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 @@ -90,12 +90,13 @@ 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): - self.assertGreaterEqual(tuple(map(int, SmdaConfig.ESCAPER_DOWNWARD_COMPATIBILITY.split("."))), (5, 0, 1)) + # 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 ( 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)