Files
pm3py/docs/plans/2026-07-16-c-client-oracle-and-lf-demod-parity.md
michael e3a394859c docs: correct the T5577 block-order finding — MAXBLK=1 test is the decisive one
The block-3 write-read-back wasn't decisive: writebl and readbl share the address
path, so they round-trip regardless of addressing correctness. Since proxmark,
Flipper, and pm3py all write the EM4100 header into block 1, ruling out a pm3py
1<->2 read-swap required an addressing-free read. Set MAXBLK=1 so regular-read
streams only physical block 1, read it off the air, and it matched READ(1)
(non-header BDEF7BC0). Confirmed: the tag is a genuine reverse-order clone, pm3py
reads it correctly, no addressing bug. Restored MAXBLK=2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:27:07 -07:00

6.2 KiB

C client oracle + LF demod parity — future-work notes

2026-07-16. Written while finishing the rawcli T5577 catalog and validating on real hardware (a T5577 emulating EM4100 00FFFFFFFF, config word 00148040).

I wanted the stock proxmark3 C client as a ground-truth oracle for LF demod — to cross-check pm3py's decode of the live tag. It wouldn't run in this environment, and digging into why surfaced the notes below. Net: there is no C-client code bug to PR from what we found — the two real items are (1) an environment blocker and (2) a pm3py-side demod-parity gap. Captured here so a future effort doesn't re-derive it.

1. The C client won't run inside the VS Code snap sandbox (oracle blocker)

  • Symptom: ./pm3 -p /dev/ttyACM0 -c 'lf search'symbol lookup error: /snap/core20/current/lib/x86_64-linux-gnu/libpthread.so.0: undefined symbol: __libc_pthread_init, version GLIBC_PRIVATE
  • Root cause: this shell runs inside the VS Code snap (SNAP_REVISION, SNAP_REAL_HOME, /snap/code/250/...). The snap runtime injects core20's older glibc / libpthread.so.0 ahead of the system libs the client was built against, so a GLIBC_PRIVATE symbol the system libc no longer exports gets looked up in the old snap libpthread and fails.
  • Confirmed it is NOT a client defect:
    • The binary has no RPATH/RUNPATH; its NEEDED libs (libpython3.12, Qt5*, readline, …) all resolve to system /lib/x86_64-linux-gnu under ldd.
    • LD_LIBRARY_PATH is unset; clearing LD_LIBRARY_PATH/GTK_PATH did not help — the injection is at the snap-runtime level, not via a variable we can scrub.
  • To use the client as an oracle: run it from a non-snap login shell / real terminal (outside VS Code's snap), or build+run it inside a matched (non-snap) environment. No proxmark3 change fixes a sandbox library injection.
  • Marginal upstream idea (only if it ever matters): the pm3 launcher could sanitize obviously-contaminating library paths before exec, but that is a weak, environment-specific band-aid — not worth a PR on its own.
  • Client checkout here: dangerous-tac0s/proxmark3 v4.20728-1264-g273777b21.

2. LF clock detection — pm3py's port was weaker than the C client (pm3py-side parity)

We found and fixed a real bug in pm3py pm3py/lf/dsp.py::detect_clock: it estimated the bit clock from the minimum edge interval, so a handful of stray sub-bit edges on a strongly-coupled, rail-clipped tag collapsed the estimate (true half-bit 32 → ~3), snapping the data clock to 8 and producing an all-phase-error ASK demod (nothing decoded). Fixed to key off the shortest well-supported interval (≥20% of the modal count) instead of the raw minimum.

The C client's DetectASKClock (common/lfdemod.c) is already robust to this — worth porting its approach for full parity:

  • It first runs DetectCleanAskWave and, for clean/strong/clipped peaks, routes to a dedicated DetectStrongAskClock — exactly the case that broke our port.
  • Otherwise it error-scores every candidate clock {8,16,32,40,50,64,100,128,272} against the wave and picks the best fit — not a single-interval heuristic.

Action (pm3py, not a C-client PR): port the candidate-clock error-scoring + a strong/clean-ASK fast path into pm3py/lf. Our support-based detect_clock fix is a partial, targeted step; the C client's scoring is the fuller solution.

3. Data-block reads — investigated to ground truth (no bug), one real improvement

Post-fix, pm3py decodes the live tag as:

  • Emitted credential: EM4100 00FFFFFFFF
  • T5577 config block 0: 00148040 (Manchester, RF/64, MAXBLK 2, ST=0, non-inverted → EM4100/EM4102) — decoded rotation-resolved against presets (reliable).
  • Data blocks: READ(1)=A108421F, READ(2)=007FE108, which at first looked wrong (not a rotation of the encoder's FF801EF7/BDEF7BC0).

Chased to ground truth against the ATA5577C datasheet + proxmark3 C source + an on-hardware write-read-back — and there is no read/addressing bug:

  • Datasheet §5.9: the direct-access command (opcode 10 + 0 + 3-bit addr — exactly what the firmware sends) reads only the addressed block, repetitively. A clean 32-bit repeat in the capture proves block-read mode engaged (the 64-bit emulation stream can't produce one).
  • T55xx_SetBits clocks the address MSB-first; pm3py's readbl payload matches the firmware struct byte-for-byte. Addressing is stock-proxmark, verified end-to-end.
  • A block-3 write-read-back was not decisive on its own: writebl and readbl share the same address path, so they round-trip regardless of whether addressing is correct (it only proved the demod inverts). Proxmark, Flipper, and pm3py all write the header into block 1, so the alternative "pm3py reads 1↔2 swapped" had to be excluded with an addressing-free read.
  • Decisive test (MAXBLK=1): temporarily set the config MAXBLK field to 1 (00148040 → 00148020) so regular-read streams only physical block 1 (datasheet §5.11.1), read it off the air (no read-command addressing), then restored 00148040. Physical block 1 came back as ~BDEF7BC0 — the non-header half — matching READ(1). So READ(1) == physical block 1.

Conclusion: this tag was simply cloned with the two EM4100 halves in reverse order (block 1 = BDEF7BC0, block 2 = FF801EF7) by a non-proxmark/Flipper/pm3py tool; both orders stream the same cyclic waveform and read as 00FFFFFFFF. pm3py reads them correctly. The "wrong" appearance was polarity (inversion) + rotation, i.e. the documented "best-effort — bit alignment not verified".

Real improvement (open): read_t55xx_block should resolve data-block polarity/rotation instead of leaving it best-effort. Polarity is determinable — read block 0 (config), resolve its polarity against the known preset, and carry that polarity to the data blocks (they share the tag's modulation). Rotation can be anchored off the block-read response start. The C client's cmdlft55xx demod already does this alignment — worth porting alongside §2. Still not cross-checked against lf t55 dump (client blocked by §1), but the write-read-back makes that non-blocking.