#9586·ghidra

AARCH64: scalar FMIN/FMAX/FMAXNM mis-lifted when Rd == Rm

Author: romanmikhailovCreated Sep 4, 2026Updated Sep 14, 2026
LabelsType: BugFeature: Processor/AARCH64Status: Internal

Describe the bug

The AArch64 SLEIGH semantics for the scalar FMIN/FMAX/FMAXNM instructions write the destination register before reading the second source operand. When an encoding names the same register for both Rd and Rm, the first assignment destroys the operand the subsequent comparison reads, and the instruction is silently mis-lifted.

Ghidra/Processors/AARCH64/data/languages/AARCH64neon.sinc, 9 constructors — 3 operations x 3 widths. The FMIN 32-bit one reads:

Rd_FPR32 = Rn_FPR32;
zext_zs(Zd);
local tmp1:1 = Rn_FPR32 f<= Rm_FPR32;
if (tmp1) goto inst_next;
Rd_FPR32 = Rm_FPR32;
zext_zs(Zd);

For fmin s0, s6, s0 (Rd == Rm == s0) the first statement overwrites s0 with s6, so the comparison evaluates s6 f<= s6, which folds to constant true. The branch becomes unconditional, the second half of the instruction is dropped, and the bound is gone from the p-code entirely:

0055fe08  fmin s0,s6,s0
  (register,0x5000,4) COPY (register,0x50c0,4)                                   ; s0 = s6  <-- clobbers Rm
  (unique,0x179c00,1) FLOAT_LESSEQUAL (register,0x50c0,4), (register,0x5000,4)   ; s6 <= s6
  ---  CBRANCH (ram,0x55fe0c,8), (unique,0x179c00,1)
  (register,0x5000,4) COPY (register,0x5000,4)                                   ; s0 = s0

The decompiler then reports WARNING: Removing unreachable block at the instruction's own address.

The FMAX form is silent, and therefore worse. There the comparison is f>, so a self-clobber folds to constant false: the branch is simply never taken, no block becomes unreachable, and no warning is emitted at all. The instruction is mis-lifted with nothing to indicate it.

Only Rd == Rm is affected. Three distinct registers lift correctly, and so does Rd == Rn, because Rd = Rn is idempotent. FMINNM is unaffected — it uses an opaque pcodeop rather than this inline pattern.

To Reproduce

  1. Write these 8 bytes to a file — fmin s0, s6, s0 followed by fmax s0, s6, s0: c0 58 20 1e c0 48 20 1e
  2. Import it into Ghidra with format Raw Binary and language AARCH64:LE:64:v8A.
  3. Disassemble at offset 0.
  4. Read the raw p-code of each instruction (Edit > Tool Options > Listing Fields > PCode Field, or the decompiler's Debug Function Decompilation).
  5. Both instructions lift as s0 = s6, with the comparison and the second assignment folded away. fmin additionally raises WARNING: Removing unreachable block; fmax raises nothing.

Expected behavior

fmin s0, s6, s0 should compute s0 = min(s6, s0), preserving the original value of s0 as an operand of the comparison.

Environment (please complete the following information):

  • OS: macOS 26.6.2
  • Java Version: OpenJDK 21.0.12.1
  • Ghidra Version: 12.1.3
  • Ghidra Origin: third party distro (Homebrew)

Additional context

The p-code above was captured on a stock, unpatched install. The same nine constructors are unchanged in master as of this report.

Found in compiler-generated ARM64 code, where the instruction arises naturally from a clamp idiom: the compiler emitted fmov s0, #1.0 followed by fmin s0, s6, s0 for an upper bound. The decompiled body lost the upper clamp entirely, so the function silently returns a wrong value for inputs above the bound rather than failing visibly.

The template also disagrees with the ARM specification on NaN and signed zero — a comparison against NaN is false, and +0 == -0 makes the two indistinguishable by comparison at all. That is pre-existing and orthogonal to the self-clobber; this report does not attempt it, and it may deserve a separate issue.

Related (not duplicates) — the same general defect class, "the destination is written before a source that aliases it is read", has been reported for other processors: #6557 (x86 XADD) and #4282 (x86 POP with stack-pointer operands). I could not find an existing report for the AArch64 scalar min/max case.

Suggested fix

Read both operands into locals before writing Rd, in all 9 constructors:

local lhs32:4 = Rn_FPR32;
local rhs32:4 = Rm_FPR32;
Rd_FPR32 = lhs32;
zext_zs(Zd);
local tmp1:1 = lhs32 f<= rhs32;
if (tmp1) goto inst_next;
Rd_FPR32 = rhs32;
zext_zs(Zd);

I have applied exactly this locally (all 9 constructors, recompiled the grammar with support/sleigh -a) and confirmed the clamp is recovered and the warning disappears, with no other function in the same binary changing output. Happy to open a PR if that is useful.

Source: NationalSecurityAgency/ghidra