diff --git a/pymodbus/pdu/decoders.py b/pymodbus/pdu/decoders.py index ba7d5b00c..d0114684b 100644 --- a/pymodbus/pdu/decoders.py +++ b/pymodbus/pdu/decoders.py @@ -68,6 +68,11 @@ def decode(self, frame: bytes) -> ModbusPDU: function_code = 0 try: if (function_code := int(frame[0])) > 0x80: + if self.is_server: + raise ModbusIOException( + f"Reserved function code {function_code}", + function_code=function_code, + ) pdu_exp = ExceptionResponse(function_code & 0x7F) pdu_exp.from_frame(False, frame) return pdu_exp diff --git a/test/framer/test_extras.py b/test/framer/test_extras.py index bc234b358..c74dc48b9 100755 --- a/test/framer/test_extras.py +++ b/test/framer/test_extras.py @@ -142,7 +142,7 @@ def test_ascii_process_incoming_packets(self): def test_rtu_decode_exception(self): """Test that the RTU framer can decode errors.""" msg = b"\x00\x90\x02\x9c\x01" - _, pdu = self._rtu.handleFrame(msg, 0, 0) + _, pdu = FramerRTU(DecodePDU(False)).handleFrame(msg, 0, 0) assert pdu def test_tcp_framer_skip_wrong_dev_id_then_decode(self): diff --git a/test/pdu/test_decoders.py b/test/pdu/test_decoders.py index 7aa9f2290..0208cf994 100644 --- a/test/pdu/test_decoders.py +++ b/test/pdu/test_decoders.py @@ -132,6 +132,13 @@ def test_server_decode(self, code, frame): assert pdu assert pdu.function_code == code + @pytest.mark.parametrize(("code", "frame"), list(exceptions)) + def test_server_decode_reserved_function_code(self, code, frame): + """A request may not use a function code reserved for exception responses.""" + with pytest.raises(ModbusIOException) as exc: + self.server.decode(frame) + assert exc.value.fcode == code + @pytest.mark.parametrize(("frame"), [b"", b"NO FRAME"]) @pytest.mark.parametrize(("decoder"), [server, client]) def test_decode_bad_frame(self, decoder, frame): diff --git a/test/pdu/test_pdu.py b/test/pdu/test_pdu.py index 8c11e5873..a5d2fb263 100644 --- a/test/pdu/test_pdu.py +++ b/test/pdu/test_pdu.py @@ -30,7 +30,7 @@ async def test_error_methods(self): """Test all error methods.""" result = self.exception.x_encode() assert result == b"\x01" - DecodePDU(True).decode(b"\x81" + result) + DecodePDU(False).decode(b"\x81" + result) assert self.exception.exception_code == 1 self.exception.x_decode(b"") diff --git a/test/server/test_server_asyncio.py b/test/server/test_server_asyncio.py index 4072109b3..84437d240 100755 --- a/test/server/test_server_asyncio.py +++ b/test/server/test_server_asyncio.py @@ -274,6 +274,14 @@ async def test_async_tcp_server_write_coil_illegal_value(self): await asyncio.wait_for(BasicClient.done, timeout=0.1) assert BasicClient.received_data == b"\x00\x00\x00\x00\x00\x03\x01\x85\x03" + async def test_async_tcp_server_reserved_function_code(self): + """A function code reserved for exception responses must answer 0x01.""" + BasicClient.data = b"\x00\x00\x00\x00\x00\x04\x01\x81\x00\x00" + await self.start_server() + await self.connect_server() + await asyncio.wait_for(BasicClient.done, timeout=0.1) + assert BasicClient.received_data == b"\x00\x00\x00\x00\x00\x03\x01\x81\x01" + async def test_async_server_trace_connect_disconnect(self): """Test connect/disconnect trace handler.""" trace_connect = mock.Mock()