Skip to content

PM5: MifareCIdent() hard-hangs the firmware when called after RATS — hf mfu info / hf search / auto kill the device on any tag #3516

Description

@PasqualePillitteri

Describe the bug

On Proxmark5 (AT32F437), CMD_HF_MIFARE_CIDENT hard-hangs the firmware when it is
executed after a RATS has been sent. The device stops answering on USB entirely — it
does not even reply to a raw CMD_PING — while still being enumerated by the host.
The watchdog does not recover it: a physical reset is required.

Because ul_magic_test() is called unconditionally by GetHF14AMfU_Type(), this makes
hf mfu info, hf search and auto kill the device on any tag, including a plain
non-magic NTAG 213.

To Reproduce

With a NTAG 213 (SAK=00) on the antenna:

pm3 --> hf mfu info

[=] --- Tag Information --------------------------
[+]       TYPE: NTAG 213 144bytes (NT2H1311G0DU)
[#] iso14443a card select timeout

The device is dead from this point on; every later command returns
ERROR: cannot communicate with the Proxmark3 until it is physically reset.

Minimal reproducer — three commands, straight over the serial port, no client involved:

  1. CMD_HF_ISO14443A_READER with ISO14A_CONNECT | ISO14A_CLEARTRACE | ISO14A_NO_DISCONNECT
  2. CMD_HF_ISO14443A_READER with ISO14A_RAW | ISO14A_APPEND_CRC | ISO14A_NO_DISCONNECT, payload E0 80 (RATS)
  3. CMD_HF_MIFARE_CIDENT, payload 8 zero bytes

Step 3 returns 0 bytes and the firmware is gone. A standalone python3 script (no
dependencies) that performs these three steps and probes liveness with a raw CMD_PING
after each one is attached below.

Expected behavior

MifareCIdent() should complete and reply, or fail gracefully. It should never leave the
firmware unresponsive.

Analysis

Neither command kills the device on its own — CIDENT alone is fine, RATS alone is fine.
Only the combination does, which matches the sensitivity the code already documents in
MifareCIdent():

// check for super card gen2
// not available after RATS, reset card before executing

The call chain from the client is:

ul_magic_test() -> ul_select_rats() (sends RATS E0 80) -> SendCommandNG(CMD_HF_MIFARE_CIDENT)

Ruled out during the investigation, each with a measurement rather than a guess:

  • cabling / power / USB hub (device is plugged straight into the host; hw version 5/5 OK)
  • antenna (hw tune: HF 35.59 V, LF 17.98 V, both reported ok)
  • the tag itself (hf 14a reader 5/5, UID stable, BCC0/BCC1 and CC all verify)
  • corrupted flash image (bootrom and fullimage reflashed, behaviour unchanged)
  • ISO14A_CLEARTRACE, field OFF->ON cycling, GET_VERSION
  • BigBuf exhaustion — measured 495740 bytes free out of 498048 right before the call,
    against the ~1 KB the function allocates

Unrelated but worth fixing while in there: the three BigBuf_calloc() results at the top
of MifareCIdent() are used as receive buffers without ever being NULL-checked, even
though BigBuf_malloc() can return NULL.

Minimal reproducer script (python3, no dependencies)
#!/usr/bin/env python3
# Minimal reproducer: MifareCIdent() hard-hangs Proxmark5 firmware when called after RATS.
# Put any ISO14443-A tag on the antenna and run. No dependencies.
import os, select, struct, time
PORT = "/dev/cu.usbmodem101"
CMD_PING, CMD_READER, CMD_DROPFIELD, CMD_CIDENT = 0x0109, 0x0385, 0x0430, 0x0607
CONNECT, NO_DISCONNECT, RAW, APPEND_CRC, CLEARTRACE = 1, 2, 8, 32, 1 << 17
fd = os.open(PORT, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)

def drain(t=0.4):
    out = b""; end = time.time() + t
    while time.time() < end:
        r, _, _ = select.select([fd], [], [], 0.1)
        if r:
            try: c = os.read(fd, 4096)
            except OSError: break
            if c: out += c
    return out

def pkt_ng(cmd, data=b""):
    return b"PM3a" + struct.pack("<HH", (len(data) & 0x7FFF) | (1 << 15), cmd) + data + b"a3"
def pkt_mix(cmd, a0, a1=0, a2=0, payload=b""):
    d = struct.pack("<QQQ", a0, a1, a2) + payload
    return b"PM3a" + struct.pack("<HH", len(d) & 0x7FFF, cmd) + d + b"a3"
def alive():
    drain(0.1); os.write(fd, pkt_ng(CMD_PING)); return len(drain(0.8)) > 0

print("initial state:", "ALIVE" if alive() else "DEAD")

os.write(fd, pkt_mix(CMD_READER, CONNECT | CLEARTRACE | NO_DISCONNECT)); drain(0.5)
print("1. select            -> %s" % ("ALIVE" if alive() else "DEAD"))
os.write(fd, pkt_mix(CMD_READER, RAW | APPEND_CRC | NO_DISCONNECT, 2, 0, b"\xE0\x80")); drain(0.5)
print("2. RAW RATS          -> %s" % ("ALIVE" if alive() else "DEAD"))
os.write(fd, pkt_ng(CMD_CIDENT, bytes(8))); r = drain(2.5)
a = alive()
print("3. CIDENT (%d bytes) -> %s" % (len(r), "ALIVE" if a else "DEAD"))
if not a:
    print("\n>>> MINIMAL REPRODUCER: select -> RATS -> CIDENT (3 commands)")
os.close(fd)

Expected output on a device that still has the bug:

initial state: ALIVE
1. select            -> ALIVE
2. RAW RATS          -> ALIVE
3. CIDENT (0 bytes) -> DEAD

Desktop

  • OS: macOS (Darwin 25.5.0, arm64)
  • Client: Iceman/master/v4.21611-843-g32e2e414c
  • ARM OS + bootrom: same build, both 2026-08-25 11:14
  • Model: PM5, uC AT32F437, external flash present

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