Skip to content

AArch64 CASP stores the new pair when the compare fails and skips the store when it succeeds #1564

Closed as duplicate

Description

@JeroenRobben

Found by comparing the semantics described in the Arm Architecture Reference Manual with how Miasm implements them. An AI agent found this bug, reproduced it with Miasm 3671fc2 and drafted this report. A second agent tried to refute it and searched upstream for duplicates. The methodology is described here.

What went wrong: Wrong result

Version: miasm 0.1.5 (tag v0.1.5, ef52b54d). Also reproduces at master 3671fc2 (2026-09-21).

Preconditions: none, default Machine("aarch64l") lifter. The code under analysis contains CASP/CASPA/CASPL/CASPAL (ARMv8.1 LSE), for example clang -march=armv8.1-a output for a 128-bit __atomic_compare_exchange_n.

Input

repro.py runs casp x0, x1, x2, x3, [x4] (0x48207c82), caspal (0x4860fc82) and casp w0, w1, w2, w3, [x4] (0x08207c82). Memory at x4 = {0x1111..., 0x2222...} and {x2, x3} = {0xaaaa..., 0xbbbb...}. Each instruction runs once with {x0, x1} equal to memory and once not equal. Every result is checked against the Arm pseudocode values (spec lines). qemu-aarch64 10.2.1 -cpu max gives the same values.

Command

PYTHON=python3 ./run.sh        # engines: symb python (add gcc for the gcc jitter)

Output

casp x equal        spec   : s0=0x1111111111111111 s1=0x2222222222222222 mem0=0xaaaaaaaaaaaaaaaa mem1=0xbbbbbbbbbbbbbbbb
casp x equal        symb   : s0=0x1111111111111111 s1=0x2222222222222222 mem0=0x1111111111111111 mem1=0x2222222222222222  WRONG
casp x equal        python : s0=0x1111111111111111 s1=0x2222222222222222 mem0=0x1111111111111111 mem1=0x2222222222222222  WRONG
casp x not-equal    spec   : s0=0x1111111111111111 s1=0x2222222222222222 mem0=0x1111111111111111 mem1=0x2222222222222222
casp x not-equal    symb   : s0=0xaaaaaaaaaaaaaaaa s1=0xbbbbbbbbbbbbbbbb mem0=0xaaaaaaaaaaaaaaaa mem1=0xbbbbbbbbbbbbbbbb  WRONG
casp x not-equal    python : s0=0xaaaaaaaaaaaaaaaa s1=0xbbbbbbbbbbbbbbbb mem0=0xaaaaaaaaaaaaaaaa mem1=0xbbbbbbbbbbbbbbbb  WRONG
caspal x equal      spec   : s0=0x1111111111111111 s1=0x2222222222222222 mem0=0xaaaaaaaaaaaaaaaa mem1=0xbbbbbbbbbbbbbbbb
caspal x equal      symb   : s0=0x1111111111111111 s1=0x2222222222222222 mem0=0x1111111111111111 mem1=0x2222222222222222  WRONG
caspal x equal      python : s0=0x1111111111111111 s1=0x2222222222222222 mem0=0x1111111111111111 mem1=0x2222222222222222  WRONG
caspal x not-equal  spec   : s0=0x1111111111111111 s1=0x2222222222222222 mem0=0x1111111111111111 mem1=0x2222222222222222
caspal x not-equal  symb   : s0=0xaaaaaaaaaaaaaaaa s1=0xbbbbbbbbbbbbbbbb mem0=0xaaaaaaaaaaaaaaaa mem1=0xbbbbbbbbbbbbbbbb  WRONG
caspal x not-equal  python : s0=0xaaaaaaaaaaaaaaaa s1=0xbbbbbbbbbbbbbbbb mem0=0xaaaaaaaaaaaaaaaa mem1=0xbbbbbbbbbbbbbbbb  WRONG
casp w equal        spec   : s0=0x11111111 s1=0x22222222 mem0=0xaaaaaaaa mem1=0xbbbbbbbb
casp w equal        symb   : s0=0x11111111 s1=0x22222222 mem0=0x11111111 mem1=0x22222222  WRONG
casp w equal        python : s0=0x11111111 s1=0x22222222 mem0=0x11111111 mem1=0x22222222  WRONG
casp w not-equal    spec   : s0=0x11111111 s1=0x22222222 mem0=0x11111111 mem1=0x22222222
casp w not-equal    symb   : s0=0xaaaaaaaa s1=0xbbbbbbbb mem0=0xaaaaaaaa mem1=0xbbbbbbbb  WRONG
casp w not-equal    python : s0=0xaaaaaaaa s1=0xbbbbbbbb mem0=0xaaaaaaaa mem1=0xbbbbbbbb  WRONG
RESULT: CASP differs from the Arm ARM (exit 1)

./run.sh symb python gcc shows the gcc jitter giving the same values. In code that uses CASP for a 128-bit compare-and-swap, the emulated CAS therefore reports success with the object unchanged. It reports failure after the object has been overwritten with the desired value.

Expected

Arm A64 ISA 2026-03, CASP: "If the comparison is equal, the values in the second pair of registers are written to memory." In the shared pseudocode CASCompare, regresult starts as oldvalue. The if oldvalue == comparevalue branch sets memresult = newvalue, and the else branch sets memresult = oldvalue. A failed compare may only write back the value it read. Either way the first register pair receives the old memory value.

Location: casp in miasm/arch/aarch64/sem.py:2093 (v0.1.5). ExprCond(FLAG_EQ_CMP(data, comp_value), loc_do, loc_store) sends the equal case past the store. loc_do also reads data back after the store. A fix must swap the targets and read the old value before the store.

Reproducer files

repro.py

"""AArch64 CASP/CASPAL in miasm vs the Arm ARM (MemAtomic/CASCompare), little-endian.
Each case: memory pair at P = {m0, m1}, compare pair {Xs, Xs+1}, new pair {Xt, Xt+1} = {A, B}.
Arm ARM: equal -> memory = {A, B}; not equal -> memory unchanged. Either way {Xs, Xs+1} = old memory."""
import sys, struct, logging
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB
from miasm.jitter.csts import PAGE_READ, PAGE_WRITE, PAGE_EXEC

CODE, DATA, P = 0x400000, 0x30000000, 0x30000800
M0, M1, A, B = 0x1111111111111111, 0x2222222222222222, 0xaaaaaaaaaaaaaaaa, 0xbbbbbbbbbbbbbbbb
CASES = [  # name, insn word, element size, s0, s1
    ("casp x equal",       0x48207c82, 64, M0, M1),   # casp x0, x1, x2, x3, [x4]
    ("casp x not-equal",   0x48207c82, 64, 5, M1),
    ("caspal x equal",     0x4860fc82, 64, M0, M1),   # caspal x0, x1, x2, x3, [x4]
    ("caspal x not-equal", 0x4860fc82, 64, M0, 6),
    ("casp w equal",       0x08207c82, 32, M0 & 0xffffffff, M1 & 0xffffffff),  # casp w0, w1, w2, w3, [x4]
    ("casp w not-equal",   0x08207c82, 32, M0 & 0xffffffff, 5),
]
RET = 0xd65f03c0  # ret

def run_symb(word, size, s0, s1, m0, m1, a, b):
    from miasm.ir.symbexec import SymbolicExecutionEngine
    from miasm.expression.expression import ExprInt, ExprMem
    logging.getLogger("asmblock").setLevel(logging.ERROR)  # only the CASP word is mapped
    mach, ldb = Machine("aarch64l"), LocationDB()
    block = mach.dis_engine(struct.pack("<I", word), loc_db=ldb).dis_block(0)
    lifter = mach.lifter_model_call(ldb); ircfg = lifter.new_ircfg(); lifter.add_asmblock_to_ircfg(block, ircfg)
    r, pair = mach.mn.regs, ExprMem(ExprInt(P, 64), 2 * size)
    sb = SymbolicExecutionEngine(lifter, {r.X0: ExprInt(s0, 64), r.X1: ExprInt(s1, 64), r.X2: ExprInt(a, 64),
                                          r.X3: ExprInt(b, 64), r.X4: ExprInt(P, 64),
                                          pair: ExprInt(m0 | (m1 << size), 2 * size)})
    pc = sb.run_at(ircfg, 0)
    while int(pc) != 4:
        pc = sb.run_at(ircfg, pc)
    mem = int(sb.eval_expr(pair))
    mask = (1 << size) - 1
    return (int(sb.eval_expr(r.X0)) & mask, int(sb.eval_expr(r.X1)) & mask, mem & mask, mem >> size)

def run(mode, word, size, s0, s1):
    mask = (1 << size) - 1
    m0, m1, a, b = M0 & mask, M1 & mask, A & mask, B & mask
    if mode == "symb":
        return run_symb(word, size, s0, s1, m0, m1, a, b)
    j = Machine("aarch64l").jitter(LocationDB(), mode)
    j.vm.add_memory_page(CODE, PAGE_READ | PAGE_WRITE | PAGE_EXEC, struct.pack("<II", word, RET) + b"\0" * 0xff8)
    j.vm.add_memory_page(DATA, PAGE_READ | PAGE_WRITE, b"\0" * 0x1000)
    fmt = "<QQ" if size == 64 else "<II"
    j.vm.set_mem(P, struct.pack(fmt, m0, m1))
    j.cpu.X0, j.cpu.X1, j.cpu.X2, j.cpu.X3, j.cpu.X4 = s0, s1, a, b, P
    j.cpu.SP = DATA + 0xf00; j.cpu.LR = CODE + 0x800
    j.add_breakpoint(CODE + 0x800, lambda jt: False)
    j.run(CODE)
    mem = struct.unpack(fmt, j.vm.get_mem(P, 2 * size // 8))
    return (j.cpu.X0 & mask, j.cpu.X1 & mask) + mem

modes = sys.argv[1:] or ["symb", "python"]
bad = 0
for name, word, size, s0, s1 in CASES:
    mask = (1 << size) - 1
    m0, m1 = M0 & mask, M1 & mask
    equal = (s0, s1) == (m0, m1)
    spec = (m0, m1) + (((A & mask, B & mask)) if equal else (m0, m1))
    line = "%-19s spec   : s0=%#x s1=%#x mem0=%#x mem1=%#x" % ((name,) + spec)
    print(line)
    for mode in modes:
        got = run(mode, word, size, s0, s1)
        ok = got == spec
        bad |= not ok
        print("%-19s %-7s: s0=%#x s1=%#x mem0=%#x mem1=%#x  %s" % ((name, mode) + got + ("OK" if ok else "WRONG",)))
print("RESULT: %s" % ("CASP differs from the Arm ARM (exit 1)" if bad else "conforms (exit 0)"))
sys.exit(1 if bad else 0)

run.sh

#!/usr/bin/env bash
# exit 1 = miasm's AArch64 CASP differs from the Arm ARM, 0 = conforms, 2 = miasm not found
# Usage: PYTHON=python3 ./run.sh [symb] [python] [gcc]   (engines; default symb python)
PYTHON=${PYTHON:-python3}
D="$(cd "$(dirname "$0")" && pwd)"
"$PYTHON" -c 'import miasm' 2>/dev/null || { echo "tool not found: miasm (set PYTHON to an interpreter with miasm installed)"; exit 2; }
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
TMPDIR="$T" "$PYTHON" -W ignore "$D/repro.py" "$@"   # fresh TMPDIR: the gcc jitter caches compiled blocks there

If this is intended behaviour or a duplicate, feel free to close it, and I'll use that as feedback to improve future reports. For questions, or to have this re-checked on a newer version of Miasm, mention @JeroenRobben or email jeroen@robben.io.

Activity

  1. Redhpm commented on Oct 28, 2026

    @Redhpm
    Contributor

    Superseded by the semantic tracking issue.
    See #1584

    Closed as duplicate as a consequence, because we need to fix semantic issues more systematically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions