mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-11-04 09:07:53 +00:00
Problem
With PR #3672/#3674 landed, CEP handshakes reliably but the SPI transport
hangs PM5 hard (needs a physical reset, no watchdog on this platform)
after some number of attach/reconnect cycles - sometimes the first
cycle, sometimes dozens of pings in. Logic-analyzer capture of a hang in
progress showed a ~1.2s burst of rapid CS-gated retries, and in one
capture, three periodic transactions after that burst got progressively
slower (2.1ms -> 3.9ms -> 3.2ms -> 5.2ms) instead of recovering.
Root causes (two, compounding)
1. cep_spi_write_sync() had seven bare `while (flag == RESET);` spins
waiting on SPI1's TDBE/BF flags, with no timeout. PM5 is the SPI
slave - TDBE only clears when the Flipper is actively clocking, on
its own ~20-120ms schedule, completely async to when PM5's reply is
ready. Queue a reply between two master polls and the wait never
returns - full main-loop death.
2. Artery's own errata sheet for the AT32F435/437 (ES0003) documents
that in SPI slave hardware-CS mode, the internal clock sync is not
guaranteed to reset on every CS falling edge - a bad sync can persist
across transactions with no self-correction. This is PM5's exact CEP
configuration (SPI_CS_HARDWARE_MODE, slave mode).
Change
armsrc/pm5_cep.c:
- cep_spi_write_sync() rewritten around a bounded cep_spi_wait_tdbe()
helper (same spin-iteration-count pattern already used for reads) -
returns PM3_EIO on timeout instead of hanging.
- New cep_spi_resync(): hardware-resets SPI1 via crm_periph_reset()
(a register re-init alone doesn't clear the errata's bad internal
state) and brings it back up. This is Artery's own documented recovery
for ES0003 ("reset SPI and restart communication"), triggered by our
existing NG-frame CRC/length checks instead of the SPI peripheral's
hardware CRC feature.
- cep_do_handshake()'s SPI-stall recovery path was itself unbounded
(retried forever as long as the CC controller reported "still
attached") and missing the StartTicks()/I2C_init(true) guard every
other I2C call site in this file has (same bug class as #3672) - now
bounded (CEP_HANDSHAKE_REPLY_MAX_RETRIES) with the guard added.
- cep_spi_data_available() now discards the Flipper's 0x55 idle-poll
filler bytes before reading the real length header, instead of ever
interpreting one as (part of) a frame length.
armsrc/cmd.c: wire cep_spi_resync() into every CEP-reachable error exit
in receive_ng_internal() (short/overflow/CRC-mismatch reads) and both
reply_old()/reply_ng_internal()'s write-failure paths, so any detected
corruption resets SPI1 before the next transaction rather than letting a
bad sync persist. Add a WITH_CEP-gated inline stub for cep_spi_resync()
so receive_ng_internal()'s call sites don't need per-site #ifdef guards
on non-CEP platforms.
Behaviour change
None for USB/FPC/BWM. CEP: a corrupted or stalled SPI1 transaction now
self-heals (one dropped reply, then normal operation) instead of
wedging the device.
Testing
- make clean && make PLATFORM=PM5 PLATFORM_EXTRAS=BWM and
make clean && make PLATFORM=PM3RDV4 - both zero warnings.
- Hardware: flashed to a real PM5 paired with a real Flipper Zero FAP.
- 60s continuous-connection capture with zero USB activity (isolates
CEP from any client-side interaction): clean, no anomalies.
- 110s capture with 18 real `hw status` calls interspersed: all 18
succeeded, CEP stayed attached throughout.
- 5 full attach/detach cycles with `hf search` against a real NTAG213
card before/during/after each attach (240s, logic-analyzer capture
throughout): all 15 reads returned the identical UID regardless of
CEP state, zero hangs, final liveness check succeeded with no reset
needed.
- Not tested: clang toolchain (firmware-only diff).
Checklist
- [x] Proper style of own code, no unrelated reformatting included
- [x] Builds warning-free with gcc (PM5 and RDV4)
- [ ] clang not tried (firmware-only diff)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
351 lines
12 KiB
C
351 lines
12 KiB
C
//-----------------------------------------------------------------------------
|
|
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// See LICENSE.txt for the text of the license.
|
|
//-----------------------------------------------------------------------------
|
|
#include "cmd.h"
|
|
#include "usb_cdc_apis.h"
|
|
#include "usb_read_ng.h"
|
|
#include "usart.h"
|
|
#ifdef WITH_BWM_FORWARD
|
|
#include "bwm_forward.h"
|
|
#endif
|
|
#ifdef WITH_CEP
|
|
#include "pm5_cep.h"
|
|
#else
|
|
// receive_ng_internal()'s `cep` parameter is always false without WITH_CEP,
|
|
// so this is never actually called - just keeps those call sites free of
|
|
// per-site #if defined(WITH_CEP) guards.
|
|
static inline void cep_spi_resync(void) {}
|
|
#endif
|
|
#include "crc16.h"
|
|
#include "string.h"
|
|
#include "BigBuf.h"
|
|
|
|
// Flags to tell where to add CRC on sent replies
|
|
bool g_reply_with_crc_on_usb = false;
|
|
bool g_reply_with_crc_on_fpc = true;
|
|
// "Session" flag, to tell via which interface next msgs should be sent: USB or FPC USART
|
|
bool g_reply_via_fpc = false;
|
|
// Independent of via_fpc - CEP (Flipper Zero, over SPI1) may be live at the same time as BWM/FPC.
|
|
bool g_reply_via_cep = false;
|
|
|
|
// Largest NG payload the device will put on the CURRENT reply link. Over FPC the
|
|
// BWM buffers are smaller than a full frame, so cap there; USB uses the full size.
|
|
uint16_t reply_ng_max_data_size(void) {
|
|
if (g_reply_via_fpc && (PM3_CMD_DATA_SIZE > PM3_FPC_MAX_DATA)) {
|
|
return PM3_FPC_MAX_DATA;
|
|
}
|
|
return PM3_CMD_DATA_SIZE;
|
|
}
|
|
bool g_reply_via_usb = false;
|
|
|
|
int reply_old(uint64_t cmd, uint64_t arg0, uint64_t arg1, uint64_t arg2, const void *data, size_t len) {
|
|
PacketResponseOLD txcmd = {CMD_UNKNOWN, {0, 0, 0}, {{0}}};
|
|
|
|
for (size_t i = 0; i < sizeof(PacketResponseOLD); i++) {
|
|
((uint8_t *)&txcmd)[i] = 0x00;
|
|
}
|
|
|
|
// Compose the outgoing command frame
|
|
txcmd.cmd = cmd;
|
|
txcmd.arg[0] = arg0;
|
|
txcmd.arg[1] = arg1;
|
|
txcmd.arg[2] = arg2;
|
|
|
|
// Add the (optional) content to the frame, with a maximum size of PM3_CMD_DATA_SIZE_OLD
|
|
if (data && len) {
|
|
len = MIN(len, PM3_CMD_DATA_SIZE_OLD);
|
|
for (size_t i = 0; i < len; i++) {
|
|
txcmd.d.asBytes[i] = ((const uint8_t *)data)[i];
|
|
}
|
|
}
|
|
|
|
#if defined(WITH_FPC_USART_HOST) || defined(WITH_BWM_FORWARD)
|
|
int resultfpc = PM3_EUNDEF;
|
|
#endif
|
|
#if defined(WITH_CEP)
|
|
int resultcep = PM3_EUNDEF;
|
|
#endif
|
|
int resultusb = PM3_EUNDEF;
|
|
// Send frame and make sure all bytes are transmitted
|
|
|
|
if (g_reply_via_usb) {
|
|
resultusb = usb_write((uint8_t *)&txcmd, sizeof(PacketResponseOLD));
|
|
}
|
|
|
|
if (g_reply_via_fpc) {
|
|
#if defined(WITH_BWM_FORWARD)
|
|
resultfpc = bwm_fwd_writebuffer_sync((uint8_t *)&txcmd, sizeof(PacketResponseOLD));
|
|
#elif defined(WITH_FPC_USART_HOST)
|
|
resultfpc = usart_writebuffer_sync((uint8_t *)&txcmd, sizeof(PacketResponseOLD));
|
|
#else
|
|
return PM3_EDEVNOTSUPP;
|
|
#endif
|
|
}
|
|
if (g_reply_via_cep) {
|
|
#if defined(WITH_CEP)
|
|
resultcep = cep_spi_write_sync((uint8_t *)&txcmd, sizeof(PacketResponseOLD));
|
|
if (resultcep != PM3_SUCCESS) {
|
|
cep_spi_resync();
|
|
}
|
|
#else
|
|
return PM3_EDEVNOTSUPP;
|
|
#endif
|
|
}
|
|
// we got multiple results, let's prioritize the faulty one and USB over FPC over CEP.
|
|
if (g_reply_via_usb && (resultusb != PM3_SUCCESS)) return resultusb;
|
|
#if defined(WITH_FPC_USART_HOST) || defined(WITH_BWM_FORWARD)
|
|
if (g_reply_via_fpc && (resultfpc != PM3_SUCCESS)) return resultfpc;
|
|
#endif
|
|
#if defined(WITH_CEP)
|
|
if (g_reply_via_cep && (resultcep != PM3_SUCCESS)) return resultcep;
|
|
#endif
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int reply_ng_internal(uint16_t cmd, int8_t status, uint8_t reason, const uint8_t *data, size_t len, bool ng) {
|
|
// The NG preamble is 10 bytes, so data[] would sit 2 past a word boundary
|
|
// and memcpy could never take its word path. Offsetting the whole frame by
|
|
// 2 lands data[] on a word boundary. The bytes on the wire are unchanged.
|
|
struct {
|
|
uint8_t pad[2];
|
|
PacketResponseNGRaw raw;
|
|
} __attribute__((aligned(4))) frame;
|
|
PacketResponseNGRaw *tx = &frame.raw;
|
|
size_t txBufferNGLen;
|
|
|
|
// Compose the outgoing command frame
|
|
tx->pre.magic = RESPONSENG_PREAMBLE_MAGIC;
|
|
tx->pre.cmd = cmd;
|
|
tx->pre.status = status;
|
|
tx->pre.reason = reason;
|
|
tx->pre.ng = ng;
|
|
if (len > PM3_CMD_DATA_SIZE) {
|
|
len = PM3_CMD_DATA_SIZE;
|
|
// overwrite status
|
|
tx->pre.status = PM3_EOVFLOW;
|
|
}
|
|
|
|
// length is only 15bit (32768)
|
|
tx->pre.length = (len & 0x7FFF);
|
|
|
|
// Add the (optional) content to the frame, with a maximum size of PM3_CMD_DATA_SIZE
|
|
if (data && len) {
|
|
memcpy(tx->data, data, len);
|
|
}
|
|
|
|
PacketResponseNGPostamble *tx_post = (PacketResponseNGPostamble *)((uint8_t *)tx + sizeof(PacketResponseNGPreamble) + len);
|
|
|
|
// Note: if we send to both FPC & USB, we'll set CRC for both if any of them require CRC
|
|
if ((g_reply_via_fpc && g_reply_with_crc_on_fpc) || ((g_reply_via_usb) && g_reply_with_crc_on_usb)) {
|
|
uint8_t first, second;
|
|
compute_crc(CRC_14443_A, (uint8_t *)tx, sizeof(PacketResponseNGPreamble) + len, &first, &second);
|
|
tx_post->crc = ((first << 8) | second);
|
|
} else {
|
|
tx_post->crc = RESPONSENG_POSTAMBLE_MAGIC;
|
|
}
|
|
txBufferNGLen = sizeof(PacketResponseNGPreamble) + len + sizeof(PacketResponseNGPostamble);
|
|
|
|
#if defined(WITH_FPC_USART_HOST) || defined(WITH_BWM_FORWARD)
|
|
int resultfpc = PM3_EUNDEF;
|
|
#endif
|
|
#if defined(WITH_CEP)
|
|
int resultcep = PM3_EUNDEF;
|
|
#endif
|
|
int resultusb = PM3_EUNDEF;
|
|
// Send frame and make sure all bytes are transmitted
|
|
|
|
if (g_reply_via_usb) {
|
|
resultusb = usb_write((uint8_t *)tx, txBufferNGLen);
|
|
}
|
|
if (g_reply_via_fpc) {
|
|
#if defined(WITH_BWM_FORWARD)
|
|
resultfpc = bwm_fwd_writebuffer_sync((uint8_t *)tx, txBufferNGLen);
|
|
#elif defined(WITH_FPC_USART_HOST)
|
|
resultfpc = usart_writebuffer_sync((uint8_t *)tx, txBufferNGLen);
|
|
#else
|
|
return PM3_EDEVNOTSUPP;
|
|
#endif
|
|
}
|
|
if (g_reply_via_cep) {
|
|
#if defined(WITH_CEP)
|
|
resultcep = cep_spi_write_sync((uint8_t *)tx, txBufferNGLen);
|
|
if (resultcep != PM3_SUCCESS) {
|
|
cep_spi_resync();
|
|
}
|
|
#else
|
|
return PM3_EDEVNOTSUPP;
|
|
#endif
|
|
}
|
|
// we got multiple results, let's prioritize the faulty one and USB over FPC over CEP.
|
|
if (g_reply_via_usb && (resultusb != PM3_SUCCESS)) {
|
|
return resultusb;
|
|
}
|
|
|
|
#if defined(WITH_FPC_USART_HOST) || defined(WITH_BWM_FORWARD)
|
|
if (g_reply_via_fpc && (resultfpc != PM3_SUCCESS)) {
|
|
return resultfpc;
|
|
}
|
|
#endif
|
|
#if defined(WITH_CEP)
|
|
if (g_reply_via_cep && (resultcep != PM3_SUCCESS)) {
|
|
return resultcep;
|
|
}
|
|
#endif
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
int reply_ng(uint16_t cmd, int8_t status, const uint8_t *data, size_t len) {
|
|
return reply_ng_internal(cmd, status, PM3_REASON_UNKNOWN, data, len, true);
|
|
}
|
|
|
|
|
|
int reply_reason(uint16_t cmd, int8_t status, int8_t reason, const uint8_t *data, size_t len) {
|
|
return reply_ng_internal(cmd, status, reason, data, len, true);
|
|
}
|
|
|
|
static int receive_ng_internal(PacketCommandNG *rx, uint32_t read_ng(uint8_t *data, size_t len), bool usb, bool fpc, bool cep) {
|
|
|
|
PacketCommandNGRaw rx_raw;
|
|
size_t bytes = read_ng((uint8_t *)&rx_raw.pre, sizeof(PacketCommandNGPreamble));
|
|
|
|
if (bytes == 0) {
|
|
return PM3_ENODATA;
|
|
}
|
|
|
|
if (bytes != sizeof(PacketCommandNGPreamble)) {
|
|
if (cep) cep_spi_resync();
|
|
return PM3_EIO;
|
|
}
|
|
|
|
rx->magic = rx_raw.pre.magic;
|
|
rx->ng = rx_raw.pre.ng;
|
|
rx->cmd = rx_raw.pre.cmd;
|
|
|
|
uint16_t length = rx_raw.pre.length;
|
|
|
|
if (rx->magic == COMMANDNG_PREAMBLE_MAGIC) { // New style NG command
|
|
if (length > PM3_CMD_DATA_SIZE) {
|
|
if (cep) cep_spi_resync();
|
|
return PM3_EOVFLOW;
|
|
}
|
|
|
|
// Get the core and variable length payload
|
|
bytes = read_ng((uint8_t *)&rx_raw.data, length);
|
|
if (bytes != length) {
|
|
if (cep) cep_spi_resync();
|
|
return PM3_EIO;
|
|
}
|
|
|
|
// Get the postamble
|
|
bytes = read_ng((uint8_t *)&rx_raw.foopost, sizeof(PacketCommandNGPostamble));
|
|
if (bytes != sizeof(PacketCommandNGPostamble)) {
|
|
if (cep) cep_spi_resync();
|
|
return PM3_EIO;
|
|
}
|
|
|
|
// Check CRC, accept MAGIC as placeholder - a mismatch here is the
|
|
// AT32 errata's own recommended trigger to reset SPI (see
|
|
// cep_spi_resync()'s comment).
|
|
rx->crc = rx_raw.foopost.crc;
|
|
if (rx->crc != COMMANDNG_POSTAMBLE_MAGIC) {
|
|
uint8_t first, second;
|
|
compute_crc(CRC_14443_A, (uint8_t *)&rx_raw, sizeof(PacketCommandNGPreamble) + length, &first, &second);
|
|
if ((first << 8) + second != rx->crc) {
|
|
if (cep) cep_spi_resync();
|
|
return PM3_EIO;
|
|
}
|
|
}
|
|
|
|
// Zero the payload only once a packet is really coming in, and only
|
|
// after all wire reads: CEP's SPI receiver has no FIFO, so a stall
|
|
// between reads (this memset touches up to PM3_CMD_DATA_SIZE bytes)
|
|
// loses data. Both branches leave the tail of data untouched -- an
|
|
// NG command fills only its own length, an old style one only
|
|
// PM3_CMD_DATA_SIZE_OLD -- so what is not written has to start zero.
|
|
memset(&rx->data, 0, sizeof(rx->data));
|
|
|
|
if (rx->ng) {
|
|
memcpy(rx->data.asBytes, rx_raw.data, length);
|
|
rx->length = length;
|
|
} else {
|
|
uint64_t arg[3] = {0};
|
|
if (length < sizeof(arg)) {
|
|
if (cep) cep_spi_resync();
|
|
return PM3_EIO;
|
|
}
|
|
|
|
memcpy(arg, rx_raw.data, sizeof(arg));
|
|
rx->oldarg[0] = arg[0];
|
|
rx->oldarg[1] = arg[1];
|
|
rx->oldarg[2] = arg[2];
|
|
memcpy(rx->data.asBytes, rx_raw.data + sizeof(arg), length - sizeof(arg));
|
|
rx->length = length - sizeof(arg);
|
|
}
|
|
|
|
g_reply_via_usb = usb;
|
|
g_reply_via_fpc = fpc;
|
|
g_reply_via_cep = cep;
|
|
|
|
} else { // Old style command
|
|
PacketCommandOLD rx_old;
|
|
memcpy(&rx_old, &rx_raw.pre, sizeof(PacketCommandNGPreamble));
|
|
bytes = read_ng(((uint8_t *)&rx_old) + sizeof(PacketCommandNGPreamble), sizeof(PacketCommandOLD) - sizeof(PacketCommandNGPreamble));
|
|
if (bytes != sizeof(PacketCommandOLD) - sizeof(PacketCommandNGPreamble)) {
|
|
return PM3_EIO;
|
|
}
|
|
|
|
g_reply_via_usb = usb;
|
|
g_reply_via_fpc = fpc;
|
|
g_reply_via_cep = cep;
|
|
rx->ng = false;
|
|
rx->magic = 0;
|
|
rx->crc = 0;
|
|
rx->cmd = (rx_old.cmd & 0xFFFF);
|
|
rx->oldarg[0] = rx_old.arg[0];
|
|
rx->oldarg[1] = rx_old.arg[1];
|
|
rx->oldarg[2] = rx_old.arg[2];
|
|
rx->length = PM3_CMD_DATA_SIZE_OLD;
|
|
memset(&rx->data, 0, sizeof(rx->data));
|
|
memcpy(&rx->data, &rx_old.d.asBytes, rx->length);
|
|
}
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
int receive_ng(PacketCommandNG *rx) {
|
|
|
|
// Check if there is a packet available
|
|
if (usb_poll_validate_length()) {
|
|
return receive_ng_internal(rx, usb_read_ng, true, false, false);
|
|
}
|
|
|
|
#if defined(WITH_CEP)
|
|
// cep_is_active() gates this so an unattached/idle CEP link doesn't cost
|
|
// an SPI-timeout poll every single main-loop iteration.
|
|
if (cep_is_active() && cep_spi_data_available())
|
|
return receive_ng_internal(rx, cep_spi_read_ng, false, false, true);
|
|
#endif
|
|
|
|
#if defined(WITH_BWM_FORWARD)
|
|
// De-frame inbound BWM app_com DATA_FORWARD packets into a raw NG stream.
|
|
if (bwm_fwd_rxdata_available() > 0)
|
|
return receive_ng_internal(rx, bwm_read_ng, false, true, false);
|
|
#elif defined(WITH_FPC_USART_HOST)
|
|
// Check if there is a FPC packet available
|
|
if (usart_rxdata_available() > 0)
|
|
return receive_ng_internal(rx, usart_read_ng, false, true, false);
|
|
#endif
|
|
return PM3_ENODATA;
|
|
}
|