AARCH64: scalar FMIN/FMAX/FMAXNM mis-lifted when Rd == Rm
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
- Write these 8 bytes to a file —
fmin s0, s6, s0followed byfmax s0, s6, s0:c0 58 20 1e c0 48 20 1e - Import it into Ghidra with format Raw Binary and language
AARCH64:LE:64:v8A. - Disassemble at offset
0. - Read the raw p-code of each instruction (Edit > Tool Options > Listing Fields > PCode Field, or the decompiler's Debug Function Decompilation).
- Both instructions lift as
s0 = s6, with the comparison and the second assignment folded away.fminadditionally raisesWARNING: Removing unreachable block;fmaxraises 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