From 9c9317b03b6f466628146183221a0ad8e147d7b8 Mon Sep 17 00:00:00 2001 From: Ali Norouzi Date: Mon, 30 Mar 2026 17:34:54 +0200 Subject: [PATCH 1/3] contrib/automotive: remove conf.debug_dissector side-effect on import --- scapy/contrib/automotive/uds.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 978bf6a0483..aeb4bb82af8 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -41,8 +41,6 @@ # "The default value is False.") conf.contribs['UDS'] = {'treat-response-pending-as-answer': False} -conf.debug_dissector = True - class UDS(ISOTP): services = ObservableDict( From 504e3b23780e541da9334fdf8c0eb0ca7fc74afb Mon Sep 17 00:00:00 2001 From: Ali Norouzi Date: Mon, 30 Mar 2026 17:35:01 +0200 Subject: [PATCH 2/3] test/imports: refactor to concurrent.futures; add conf.debug_dissector check The old import_all had three bugs: filenames could be silently dropped when the pool was saturated (no blocking wait for a free slot), the final batch was never drained, and the opening while-loop was dead code. Replace append_processes/check_processes/import_all with ThreadPoolExecutor, which handles queuing and draining correctly and cuts total test time roughly in half. Add a test verifying no module sets conf.debug_dissector as a side-effect of import, using the same ALL_FILES exclusion list and subprocess isolation as the existing import tests. --- test/imports.uts | 70 +++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/test/imports.uts b/test/imports.uts index ad6ca83598e..34eee6199b8 100644 --- a/test/imports.uts +++ b/test/imports.uts @@ -10,8 +10,8 @@ import os import glob import subprocess import re -import time import sys +from concurrent.futures import ThreadPoolExecutor, as_completed from scapy.consts import WINDOWS, OPENBSD # DEV: to add your file to this list, make sure you have @@ -49,46 +49,19 @@ ALL_FILES = [ NB_PROC = 1 if WINDOWS or OPENBSD else 4 -def append_processes(processes, filename): - processes.append( - (subprocess.Popen( - [sys.executable, "-c", "import %s" % filename], - stderr=subprocess.PIPE, encoding="utf8"), - time.time(), - filename)) - -def check_processes(processes): - for i, tup in enumerate(processes): - proc, start_ts, file = tup - errs = "" - try: - _, errs = proc.communicate(timeout=0.5) - except subprocess.TimeoutExpired: - if time.time() - start_ts > 30: - proc.kill() - errs = "Timed out (>30s)!" - if proc.returncode is None: - continue - else: - print("Finished %s with %d after %f sec" % - (file, proc.returncode, time.time() - start_ts)) - if proc.returncode != 0: - for p in processes: - p[0].kill() - raise Exception( - "Importing the file '%s' failed !\\n%s" % (file, errs)) - del processes[i] - return - - def import_all(FILES): - processes = list() - while len(processes) == NB_PROC: - check_processes(processes) - for filename in FILES: - check_processes(processes) - if len(processes) < NB_PROC: - append_processes(processes, filename) + with ThreadPoolExecutor(max_workers=NB_PROC) as executor: + futures = { + executor.submit(subprocess.run, + [sys.executable, "-c", "import %s" % f], + stderr=subprocess.PIPE, encoding="utf8", timeout=30): f + for f in FILES + } + for future in as_completed(futures): + result = future.result() + if result.returncode != 0: + raise Exception( + "Importing the file '%s' failed !\n%s" % (futures[future], result.stderr)) = Try importing all core separately @@ -102,3 +75,20 @@ import_all(x for x in ALL_FILES if "layers" in x) = Try importing all contribs separately import_all(x for x in ALL_FILES if "contrib" in x) + += Verify no module modifies conf.debug_dissector on import + +with ThreadPoolExecutor(max_workers=NB_PROC) as _executor: + _futures = { + _executor.submit(subprocess.run, + [sys.executable, "-c", + "import %s; from scapy.config import conf; " + "assert not conf.debug_dissector, " + "'%s set conf.debug_dissector as a side-effect'" % (f, f)], + stderr=subprocess.PIPE, encoding="utf8", timeout=30): f + for f in ALL_FILES + } + for _future in as_completed(_futures): + _result = _future.result() + assert _result.returncode == 0, \ + "%s set conf.debug_dissector as a side-effect\n%s" % (_futures[_future], _result.stderr) From b3204b5144d6a5273801a7377ca34526f26af2b1 Mon Sep 17 00:00:00 2001 From: Ali Norouzi Date: Mon, 30 Mar 2026 17:35:07 +0200 Subject: [PATCH 3/3] tls: fix TLS_Ext_KeyShare_SH server_share consuming trailing extensions PacketField has no length bound, so server_share greedily consumed all remaining bytes including subsequent extensions. Switch to PacketLenField with length_from=pkt.len to bound dissection to the extension's own length field. --- scapy/layers/tls/keyexchange_tls13.py | 4 ++-- test/scapy/layers/tls/tls13.uts | 32 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/scapy/layers/tls/keyexchange_tls13.py b/scapy/layers/tls/keyexchange_tls13.py index 03692ffdccb..dbebd81995d 100644 --- a/scapy/layers/tls/keyexchange_tls13.py +++ b/scapy/layers/tls/keyexchange_tls13.py @@ -15,7 +15,6 @@ from scapy.fields import ( FieldLenField, IntField, - PacketField, PacketLenField, PacketListField, ShortEnumField, @@ -157,7 +156,8 @@ class TLS_Ext_KeyShare_SH(TLS_Ext_Unknown): name = "TLS Extension - Key Share (for ServerHello)" fields_desc = [ShortEnumField("type", 0x33, _tls_ext), ShortField("len", None), - PacketField("server_share", None, KeyShareEntry)] + PacketLenField("server_share", None, + KeyShareEntry, length_from=lambda pkt: pkt.len)] def post_build(self, pkt, pay): if not self.tls_session.frozen and self.server_share.privkey: diff --git a/test/scapy/layers/tls/tls13.uts b/test/scapy/layers/tls/tls13.uts index 7a525ef1090..b9c5b1b5291 100644 --- a/test/scapy/layers/tls/tls13.uts +++ b/test/scapy/layers/tls/tls13.uts @@ -1212,6 +1212,38 @@ assert ch.len == 103 assert ch.client_shares[0].kxlen == 97 assert len(ch.client_shares[0].key_exchange) == 97 += TLS_Ext_KeyShare_SH - dissect raw bytes with PacketLenField + +from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_KeyShare_SH +from scapy.packet import Raw + +# Raw bytes of TLS_Ext_KeyShare_SH with an x25519 server key (RFC 8448 test vector) +x25519_pub_rfc8448 = 'c9828876112095fe66762bdbf7c672e156d6cc253b833df1dd69b1b04e751f0f' +raw_ks = bytes.fromhex( + '0033' # type = key_share (0x0033) + '0024' # len = 36 (4 header + 32 key bytes) + '001d' # group = x25519 (29) + '0020' # kxlen = 32 + + x25519_pub_rfc8448 +) +pkt = TLS_Ext_KeyShare_SH(raw_ks) +assert pkt.type == 0x0033 +assert pkt.len == 36 +assert pkt.server_share.group == 29 +assert pkt.server_share.kxlen == 32 +assert pkt.server_share.key_exchange == bytes.fromhex(x25519_pub_rfc8448) + +# Trailing bytes (simulating a subsequent extension) must not be consumed by server_share +trailing = bytes.fromhex('002b00020304') # TLS_Ext_SupportedVersion_SH (TLS 1.3) +pkt2 = TLS_Ext_KeyShare_SH(raw_ks + trailing) +assert pkt2.server_share.group == 29 +assert pkt2.server_share.kxlen == 32 +assert len(pkt2.server_share.key_exchange) == 32 +assert pkt2.server_share.key_exchange == bytes.fromhex(x25519_pub_rfc8448) +# Trailing bytes must be preserved as payload, not silently consumed by server_share +assert Raw in pkt2 +assert pkt2[Raw].load == trailing + = Parse TLS 1.3 Client Hello with non-rfc 5077 ticket from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_PreSharedKey_CH