Fix EF_SPDI decode crash: parse inner '80' TLVs, emit dict plmns

stk_test1 msg 14036 regression: READ BINARY of EF_SPDI returned
"No decoded data available" — _decode_spdi (added v1.16.0) parsed
the A3 container value as bare 3-byte records (missing the inner
'80' value TLVs) and returned a list of strings, while
_build_summary/_file_summary expects the EF_PLMNwAcT shape
(list of {mcc, mnc} dicts). The resulting TypeError aborted the
whole message decode — this was the source of the 28 "Decode
error ... string indices" messages seen across all sessions.

_decode_spdi now unwraps inner '80' TLVs (bare-records fallback
kept) and always returns {'plmns': [{mcc, mnc}...]}. Replay of all
33 sessions: decode exceptions 28 → 0 (2 remaining undecoded tpdu
are 1-byte truncated capture records, not decodable by nature).

Tests: +3 (standard inner-80 form, bare-records fallback, exact
stk_test1 regression via decode_message). 344 total. Version
1.17.1, SW cache simtrace-analyser-v25.
This commit is contained in:
catarrh committed 2026-08-24 13:03:31 +03:00
1 parent 07deab9c26
commit 14445ae38f
7 files changed
+48 -14

No files matched your search

+1 -1
View File
@@ -1901,7 +1901,7 @@ function onFilterChange() {
} }
// ──────────────────── Versioning ──────────────────── // ──────────────────── Versioning ────────────────────
const PWA_VERSION = '1.17.0'; const PWA_VERSION = '1.17.1';
const MIN_SERVER_VERSION = '1.0.0'; const MIN_SERVER_VERSION = '1.0.0';
function parseVersion(v) { function parseVersion(v) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simtrace-analyser-v24'; const CACHE = 'simtrace-analyser-v25';
const PRECACHE = [ const PRECACHE = [
'index.html', 'index.html',
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project] [project]
name = "simtrace2-pysniff" name = "simtrace2-pysniff"
version = "1.17.0" version = "1.17.1"
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)" description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
readme = {file = "README.md", content-type = "text/markdown"} readme = {file = "README.md", content-type = "text/markdown"}
license = {text = "GPL-2.0-or-later"} license = {text = "GPL-2.0-or-later"}
+1 -1
View File
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
from .pcap import PcapWriter from .pcap import PcapWriter
from .dump import FileDumper, format_message from .dump import FileDumper, format_message
__version__ = '1.17.0' __version__ = '1.17.1'
__all__ = [ __all__ = [
'SniffSession', 'SniffSession',
'DeviceDisconnected', 'DeviceDisconnected',
+20 -7
View File
@@ -3243,19 +3243,32 @@ def _decode_smsp(raw, p1=None):
def _decode_spdi(raw, p1=None): def _decode_spdi(raw, p1=None):
"""EF_SPDI (§4.2.66): 'A3' container with PLMN records (§0.3).""" """EF_SPDI (§4.2.66): 'A3' container (or bare '80' value tags) with
3-byte PLMN records — same shape as EF_PLMNwAcT for _file_summary."""
if all(b == 0xff for b in raw): if all(b == 0xff for b in raw):
return {'empty': True} return {'empty': True}
for tag, _length, value in parse_tlv(raw):
if tag == 0xA3:
plmns = [] plmns = []
def collect(value):
for i in range(0, len(value) - 2, 3): for i in range(0, len(value) - 2, 3):
plmn = _decode_plmn(value[i:i + 3]) plmn = _decode_plmn(value[i:i + 3])
if plmn: if plmn and plmn not in plmns:
plmns.append(f"{plmn['mcc']}-{plmn['mnc']}") plmns.append(plmn)
if plmns:
return {'plmns': plmns if len(plmns) > 1 else plmns[0]} for tag, _length, value in parse_tlv(raw):
if tag == 0xA3:
got = False
for t2, _l2, v2 in parse_tlv(value):
if t2 == 0x80:
collect(v2)
got = True
if not got:
collect(value) # bare records inside A3
elif tag == 0x80:
collect(value)
if not plmns:
return {'raw': raw.hex().upper()} return {'raw': raw.hex().upper()}
return {'plmns': plmns}
def _decode_spn(raw, p1=None): def _decode_spn(raw, p1=None):
+23 -2
View File
@@ -1976,8 +1976,29 @@ class TestNewFileDecoders(unittest.TestCase):
def test_spdi(self): def test_spdi(self):
from simtrace2_pysniff.server.decode import _decode_spdi from simtrace2_pysniff.server.decode import _decode_spdi
r = _decode_spdi(bytes.fromhex('a30652f01000f110'), p1=None) # Standard form: A3 wrapping inner '80' TLVs, one PLMN each
self.assertEqual(r['plmns'], ['250-01', '001-01']) r = _decode_spdi(bytes.fromhex('a30a8003' '52f010' '8003' '72f010'), p1=None)
self.assertEqual(r['plmns'], [{'mcc': '250', 'mnc': '01'},
{'mcc': '270', 'mnc': '01'}])
def test_spdi_bare_records_fallback(self):
from simtrace2_pysniff.server.decode import _decode_spdi
r = _decode_spdi(bytes.fromhex('a306' '52f010' '00f110'), p1=None)
self.assertEqual(r['plmns'], [{'mcc': '250', 'mnc': '01'},
{'mcc': '001', 'mnc': '01'}])
def test_spdi_read_binary_regression(self):
# stk_test1 session msg 14036: READ BINARY of EF_SPDI crashed
# _build_summary (plmns were strings, not dicts) → whole message
# decoded as None ("No decoded data available").
prev = {'sel': {'fid': '6fcd', 'name': 'EF_SPDI',
'structure': 'transparent'},
'ins': 0xA4, 'ins_name': 'SELECT', 'sw1': '61'}
r = decode_message(bytes.fromhex(
'00b000000da305800352f020ffffffffffff9000'), prev=prev)
self.assertEqual(r['file']['plmns'],
[{'mcc': '250', 'mnc': '02'}])
self.assertIn('250/02', r['summary'])
def test_smsp(self): def test_smsp(self):
from simtrace2_pysniff.server.decode import _decode_smsp from simtrace2_pysniff.server.decode import _decode_smsp
+1 -1
View File
@@ -197,7 +197,7 @@ class TestImportEndpoint(unittest.TestCase):
class TestVersion(unittest.TestCase): class TestVersion(unittest.TestCase):
def test_package_version(self): def test_package_version(self):
from simtrace2_pysniff import __version__ from simtrace2_pysniff import __version__
self.assertEqual(__version__, '1.17.0') self.assertEqual(__version__, '1.17.1')
def test_status_includes_version(self): def test_status_includes_version(self):
import os import os