The audit's P2 items:
- ODD_INS: add 0x73 MANAGE SECURE CHANNEL and 0x75 TRANSACT DATA
(TS 102 221 Table 10.5); the tools/vs_reader.py mirror and the pinned
test are updated.
- 9E/9F are SW1: ISO 7816-3 10.3.3 Table 11 makes every 9X value SW1.
Remove the fabricated "extended-length procedure byte" that consumed
SW2 as a length and lost the status word.
- T=1: EOF-guard every block read, bound the M-bit chain with
T1_CHAIN_MAX, and drop the caller's duplicate chain loop -- it read the
next block after every I-block (usually the peer's R-block) and merged
the following I-block's INF into the packet. R/S blocks emit nothing;
the (possibly chained) I-block INF is the APDU payload.
- Parity: a packet containing a parity-error byte is emitted with
GSMTAP_FLAG_BAD_FCS (the CHKSUM annotation was invisible downstream),
with a log note when the exchange is otherwise clean.
- EOF guards: handle_atr (TA/TB/TC/TD, historical, TCK), handle_pps
(request/response fields incl. PPS1-3) and the T=1 block parser return
cleanly on a truncated capture instead of raising on None.
- Dead code: _edge_read was initialized True and never cleared, so the
ATR-based T=0 procedure loop (58 lines) and the 1.5-ETU guard branch
were unreachable. Remove the flag, both dead paths and the branch;
dedent the live loop (behavior unchanged).
- Samplerate scaling: _samples_for_us() replaces the hard-coded 16 MHz
thresholds -- start-bit deglitch (0.125 us), min_high floor (0.25 us)
and the ETU glitch floor (0.75 us). At 16 MHz the values are
unchanged.
Validation: 130/130 unit tests (13 new: 9E/9F SW1, parity BAD_FCS,
T=1 I/R/S blocks + M chain + truncations + one-block-per-step, PPS
truncations, truncated ATR TA, samplerate/ETU-floor vectors); the
10-trace suite is unchanged from v1.9.6 (identical APDU/CHKSUM/CONCAT/
BAD_FCS counts, RESULT: OK); compare_baseline.sh reports IDENTICAL.
VERSION -> 1.9.7.
The audit's P1 robustness findings:
- _etu_candidate() validates a measured burst: requires >=2 usable
spacings, rejects a guard-dominated burst (all-zero data: 10-ETU LOW
runs + a 2-5 ETU HIGH guard) and a lone 10-ETU spacing. A failed
measurement leaves bit_samples unset instead of committing the
clock_skip*3 fallback.
- _confirm_etu() no longer counts a dropped frame (empty bits) as
parity-OK, and a rejected measurement clears bit_samples,
_etu_measured and _clock_skip_confident (was: kept sampling at the
rejected rate).
- Hunt ETU recovery keys off _etu_measured: handle_atr pre-seeds
bit_samples, so the old "bit_samples is None" condition never fired.
- resync_idle() waits on the DATA idle level (plus RST/VCC edges,
bounded) instead of spinning on wait({'skip': 0}) at one sample.
- ATR acceptance: a DATA-state 0x3B/0x3F is a candidate only before the
first APDU (_apdu_seen); a parsed ATR is ignored on any parity error
or when it is a bare TS+T0. A data run misread at a frame boundary
could fabricate an ATR and force clock_skip=372 for the capture.
- Inverse convention per ISO 7816-3 7.1/8.1: idle HIGH and a falling
start bit in both conventions, msb-first inverse data, raw moments
5-7 TS-pattern detection (a real inverse TS reads as 0x03 through
the direct reader), and a TS parity exemption (the inverse pattern
is odd over moments 2-10). Spec-verified; no inverse capture in the
corpus.
Validation: 117/117 unit tests (18 new); the 10-trace suite is
unchanged from v1.9.5 (APDU/CHKSUM/CONCAT/BAD_FCS identical,
RESULT: OK everywhere); compare_baseline.sh reports IDENTICAL.
VERSION -> 1.9.6.
- Decode the first post-header byte as SW1 before the ACK/data decision:
a case-2 rejection (6A82/6CXX) carries no ACK and used to be read as
command data, silently consuming the exchange and the next one.
- Short responses (Lr < Le): after the P3-byte read, split the buffer at
the first plausible SW1 followed by a plausible CLA, replay the tail and
flag BAD_FCS; previously the SW scan discarded the real SW, merged the
following exchanges and lost the exchange at EOF.
- P3=0 is Le=256 for an outgoing command: keep the response data (read
until SW) instead of discarding it, including data bytes equal to INS.
- ~INS ACK: one data byte per ACK via the new helpers; case-4 inline data
is appended (permissive; ISO 7816-3 §12.2 maps case 4 to GET RESPONSE).
- tests: four TestDecodeStep vectors (immediate SW, short read + split +
replay, Le=256 data incl. an INS-valued byte); 99/99.
- AGENTS v1.9.5 + T=0 section, README/README_RUS bullets; baseline
regenerated (xiaomi_mi_a1_coldboot 473 -> 475 APDUs, all else identical);
compare_baseline: IDENTICAL.
- tests: TestDecodeStep drives decode_step() with a scripted byte queue
(read/peek/consume stubbed over _replay + the queue; the re-align/PPS/
ATR/annotation sinks stubbed): 'CX'/'EX' exchanges frame and emit clean,
0x5F frames as a command (it was always eaten by the idle-noise gate
before v1.9.3), the 0x61 text-byte boundary is a one-byte suspicious-CLA
discard, the invalid-INS re-frame recovers a shifted SELECT, 0xFE still
takes the idle-noise path.
- tests: TestAtrHuntDetect drives handle_atr(): 0xC4 0xA4 / 0xC4 0x89
hand the header to DATA via _replay, CLA 0x00 stays excluded, an invalid
INS does not hand off.
- pd.py: the hunt command-detect guards peek_byte() returning None (a
capture ending right after a plausible CLA byte raised TypeError, seen
only by decode()'s crash protection);
test_eof_after_plausible_cla_is_clean pins the clean return.
- AGENTS v1.9.4 section; baseline regenerated (version + test listing;
compare_baseline: IDENTICAL); 95/95 unit tests.
- pd.py/AGENTS: the rejected '8X' range now reads 0x81-0x83/0x85-0x8F
(0x84 stays accepted); the 0x40-0x4F vs 0x50-0x5F citations are split
(chaining is ISO-only, TS 102 221 keeps b5=0); the bit-dense CLA list
gains 0xE7 (exact set verified).
- README_RUS.md: the invalid-INS note mirrored (the removed odd-LSB rule,
citation corrected to 5.1.2).
- tests: test_vs_reader_mirror (pd.py <-> tools/vs_reader.py drift guard),
test_odd_ins_set_pinned, the 0xE7 noise vector, and the 'EX' vector now
uses STATUS (e2 f2) as TS 102 221 Table 10.5 defines.
- tests/baseline.txt regenerated (test listing only); compare_baseline:
IDENTICAL.
- handle_pps(): flag the first post-PPS character; _read_byte_clk() re-measures
the CLK period there and accounts the measurement's 16 edges. The Alcor
reader upclocks 4.0 -> 4.8 MHz at PPS (FI/DI still 512/32), so the ATR
measurement was stale: 64 vs 53.33 samples/ETU, no post-PPS APDU framed.
- _wait_clk_rising(): project to the n-th edge (skip to target - half a
period, take one edge) instead of "skip n-2 cycles + 2 edges", which lost
~1 edge per call on fractional periods and slid the samples ~0.5 ETU early
by the parity bit. xiaomi 7698 -> 2, 4gmodem 7218 -> 0, S21p_sim2 394 -> 0
CHKSUM lines (previously misread as dirty-wire noise).
- CONCAT gate: EMBEDDED_CLA excludes the structural 0x4x/0x5x classes (payload
false hit on the Alcor secured packet); mirrored in tools/vs_reader.py and
covered by a new vector in tests/test_gsmtap.py.
- tools/capture_baseline.sh: add the alcor_reader_sample fixture; baseline
regenerated (APDU counts and RESULT: OK unchanged everywhere, 0 CONCAT).
- 75/75 unit tests. README/AGENTS updated.
v1.9.0 -- CLK frequency in RST events:
- RST events (sub_type 0x10) append the measured CLK frequency when known:
[direction, level, flags, clk_hz_be32], flags bit 0 = rate present; the
3-byte form is kept when the rate is not yet measured. Value is
samplerate / _samples_per_clock from the existing 16-edge measurement (PPS
changes F/D, not the CLK rate). VCC events unchanged. Backward
compatible (old consumers read the first two bytes only); consumers can
compute data_rate = clk_hz x D / F from the ATR/PPS Fi/Di.
- Unit tests for the payload codec; README/README_RUS/AGENTS updated.
v1.9.1 -- T=0 NULL is not an SW1:
- The 'scan for SW1' loops accepted any 0x6X byte, including the NULL
procedure byte 0x60 (card busy), so 'NULL + real SW1' was mis-framed as
the status word: the phone traces' SELECT MF came out as ...3f00 6061 —
real SW1 0x61 eaten as SW2, real SW2 0x29 left on the wire and discarded
as a suspicious CLA (desync cascade).
- plausible_sw() now accepts 0x61-0x6F and 0x90-0x9F only, per
ISO 7816-3:2006 10.3.3 Table 11 ('6X' or '9X', EXCEPT '60'; ISO 7816-4
enforces '60' as an invalid SW1) and UICC_SPECS.md 1.8. All seven
SW1-detection sites use plausible_sw(); NULLs met while scanning are
logged as rate-limited wait extensions (_note_pre_sw) instead of
suspicious bytes.
- tools/vs_reader.py::_plausible_sw() mirrors the fix; regression vector
for the 00a40004023f006061 artifact.
Validation: 74/74 unit tests; baseline regenerated and IDENTICAL (the SW
fix is count-neutral -- only the SW bytes change); all 9 example pcaps
regenerated -- vs_reader OK, zero APDUs ending in 60 61, SELECT MF now
61 29 / 61 25.
- Split the PPS exchange into the two standard GSMTAP-SIM subtypes:
0x02 (PPS_REQ, raw request bytes, span ss..es_req) and 0x03 (PPS_RSP,
raw response bytes, span es_req..end), replacing the single 0x02 packet
with concatenated request+response that deviated from libosmocore's
gsmtap.h. Wireshark now dissects both halves; simtrace2-pysniff needs
no change (its importer already maps 0x03 to 'pps' and _decode_pps()
parses the 0xFF-leading response frame). PPS annotation/log unchanged.
- gsmtap_stream.py: GSMTAP_SIM_PPS_REQ/RSP constants; header field labels
aligned with libosmocore's struct (signal_dbm/snr_db).
- Docs: GSMTAP event tables (EN/RU) updated, PPS deviation note removed;
corrected the res-byte flag attribution -- upstream simtrace2 never
sets res, so the GSMTAP_FLAG_BAD_FCS carriage is this decoder's own
extension (semantics mirror simtrace2's USB data flags).
- Tests: assert the PPS subtype values; baseline regenerated (only the
version line and test listing differed -- no decode-metric changes).
68/68 unit tests.
Newer libsigrokdecode signals end-of-capture from wait() by raising
EOFError("samples exhausted"); Linux 0.5.3 instead returns None / raises
SystemError, so this only reproduced on Windows. The framework explicitly
accepts EOFError out of decode() as normal termination, but decode() caught
it in the generic except Exception handler -- printing a traceback, logging
DECODE ERROR, and resyncing up to 50 times, which flooded the log at the end
of every capture.
- decode() and the five "except SystemError: raise" guards now use
"except (SystemError, EOFError): raise", letting the end-of-stream signal
propagate for a clean exit. The critical guard is
_measure_clock_period(), whose try wraps a wait() loop.
- 2 new unit tests assert decode() re-raises EOFError and SystemError.
- 67/67 unit tests; test_8/test_7/xiaomi_phone unchanged (Linux path).
VERSION -> 1.7.1.
The decoder could end decode() early on an idle/busy live line; the frontend
symptom was a flood of "sr_session_stop: session was NULL" after the session
stopped. Harden the EOF detection and stop the capture from dropping.
pd.py:
- _at_eof(): a step that consumes only queued bytes (_replay / peeked_byte,
e.g. the invalid-INS re-frame) no longer counts as end-of-capture; bounded
to 64 consecutive no-advance steps.
- _stall_check(track=False): the idle-LEVEL wait in wait_data_falling returns
immediately at the same sample on an idle line -- not EOF. Only "pins is
None" ends the wait there; sample-stall detection is kept for the edge wait.
- resync_idle(): guard wait()==None (was an unguarded pins[...] TypeError at
stream end).
Effect: samsung_phone_sample.sr had halted at ~21.9M/320M samples with 1 APDU;
it now decodes through ~293M to a true end (939 APDUs, CHKSUM 176). The 6
CONCAT packets are real mis-framed STK bursts now reachable (known Samsung
de-concatenation family). All other traces unchanged.
start.sh:
- Auto-restart is now the default (--no-loop disables) so an intermittent
FX2 USB stop reconnects instead of ending the session.
- Per-session pcap suffix (FILE.N.ext) so a restart never overwrites the
previous capture.
- Filter the repetitive "sr_session_stop: session was NULL" teardown line
from stderr (real decoder errors still pass).
Validation: test_8 56/0/OK, test_7 65/0/OK, xiaomi_phone 640, xiaomi_cold
473, 4gmodem 327, A55 407/395, S21p 399/375, target S21p 369; 0 CONCAT;
65/65 unit tests (10 new). VERSION -> 1.7.0.
Add hunt-side live ETU recovery so a card already at a non-default F/D
(e.g. Samsung F=512/D=32 -> 16 CLK/bit) can be decoded without a 372-rate
ATR, and bound the all-0xFF hunt so an idle/unsyncable line can never loop
forever.
- _recover_etu_in_hunt(): measure the live ETU from DATA edges while still
in FIND START, then re-derive clock_skip; reuses _confirm_etu validation.
- _atr_hunt_failure_action(): pure recover/retry/resume_data decision, with
ATR_HUNT_ETU_ATTEMPTS=3 / ATR_HUNT_IDLE_RESETS=3 / ATR_HUNT_FAIL_LIMIT=12.
- Persistent counters reset only on a successful ATR parse (not RST re-arm),
so RST-cycling phones cannot starve the escape.
- 8 new unit tests (55 total).
Validated on the full trace suite (test_8 56/0/OK, test_7 65/0/OK, xiaomi
640, xiaomi_cold 473, 4gmodem 327, A55 407/395, S21p 399/375, target S21p
369; 0 CONCAT, RESULT: OK) with no regressions.
- pd.py: remove _lock_etu_from_edges (long-disabled) and the no-op
while-True FIND START wrapper.
- pd.py: re-raise SystemError (libsigrokdecode's clean EOF signal) in the
six 'except Exception: pass' guards so put()/wait() at end-of-capture
exits cleanly instead of being swallowed.
- pd.py: fix _measure_clock_period docstring (averages, not robust-min);
drop stale 'line 1633' comment; document the synthetic pcap UDP header
(zero IP checksum / fixed loopback+ports are intentional).
- signal_diagnostic.py: add --max-samples cap + document whole-capture
RAM cost; fix stale '--rst/--vcc=ignore' guidance (-> --no-rst/--no-vcc).
- tests: make test_valid_cla_interindustry_classes use subTest (was a
no-op tuple expression).
- start.sh/start.bat: note the -A multi-spec form is sigrok-cli 0.7.x.
- Remove stale tests/baseline_new.txt (v1.1.3-era, ATR-included mode).
- Add top-level LICENSE (GPL-2.0-or-later).
- pd.py ts_from_samplenum: drop c_uint32 truncation + dead sampleOverflowCount
(pcap timestamps wrapped backwards past ~4.5 min at 16 MHz).
- pd.py handle_atr: guard None reads at EOF (was TypeError (None & 0x80) on a
capture truncated mid-ATR-hunt).
- start.bat: add --protocol (+ :protocol=T=0 default) to match start.sh's
mid-session decode; correct the 'flag-for-flag' header comment.
- start.sh: reject --rst/--vcc assigned to the same pin (was unchecked).
- setup.bat: set DEVICE_MSG on blocked/skipped probe so the trailing
'unknown' message is suppressed.
- vs_reader.py: RESULT now fails on CONCAT / payload mismatch / suspicious
CLA, not just GARBAGE (OK text unchanged; clean traces stay identical).
Residual CONCAT mis-frames (4gmodem/A55_sim2/S21p_sim2) traced to a silent
5-byte drop at an invalid-INS T=0 header (0x0f 00 a4 08 04). Drop only the
stray leading byte and re-frame the remaining four via _replay so the real
command (00 a4 08 04 02 2f 00 -> 61 1c) is recovered.
- _note_discard()/_consume_first(): every consumed-but-unemitted byte is
now logged; idle 0x00/0xFF discards are rate-limited, real data always
gets an ANN_WARN annotation.
- All read-until-SW loops log skipped non-SW bytes.
- All traces: CONCAT 0; BAD_FCS/GARBAGE 0; test_8/test_7 RESULT OK;
47/47 unit tests; baseline byte-identical.
History rewritten to a single root commit so no example trace is stored
in the repo (fixtures are local-only, gitignored). README keeps only
generic <trace>.sr placeholders; AGENTS.md retains the fixture names.