Skip to content

Validate FC23 write count against MAX_WRITE_COUNT, not MAX_READ_COUNT - #3016

Merged
janiversen merged 1 commit into
pymodbus-dev:devfrom
dylanpulver:fix-fc23-write-count-limit
Sep 1, 2026
Merged

Validate FC23 write count against MAX_WRITE_COUNT, not MAX_READ_COUNT#3016
janiversen merged 1 commit into
pymodbus-dev:devfrom
dylanpulver:fix-fc23-write-count-limit

Conversation

@dylanpulver

Copy link
Copy Markdown
Contributor

ReadWriteMultipleRegistersRequest.encode() checks the write count against MAX_READ_COUNT (125) instead of MAX_WRITE_COUNT (121), so the client encodes requests that are over the MODBUS size limit. MAX_WRITE_COUNT has no other reference in the repo.

#2997 introduced the constants and rewrote the two literals:

-        self.verifyCount(125, count=self.read_count)
-        self.verifyCount(121, count=self.write_count)
+        self.verifyCount(self.MAX_READ_COUNT, count=self.read_count)
+        self.verifyCount(self.MAX_READ_COUNT, count=self.write_count)

The second line should have been MAX_WRITE_COUNT. Measured on released 3.15.0 vs 3.14.0 with readwrite_registers(..., values=[0]*122):

version 122 write registers
3.14.0 ValueError: 1 <= count 122 <= 121 !
3.15.0 encodes; FramerRTU.buildFrame puts 257 bytes on the wire

The FC23 request PDU is 10 + 2N bytes, so N = 121 is 252 and N = 122 is 254, past the 253 byte PDU maximum (RTU ADU 257 > 256). That derivation lands on the same 121 the spec gives as 0x0079, and on the same value this PDU's own datastore_update() already enforces server-side (1 <= write_count <= 0x079) — which is why a pymodbus client can now send a request a pymodbus server answers with ILLEGAL_VALUE.

decode() is deliberately left at MAX_READ_COUNT. Raising there makes DecodePDU.decode() swallow the frame and the server answer nothing, replacing a correct exception response with silence. The second test pins that.

Existing coverage could not catch this: test_register_read_requests_count_errors goes through datastore_update(), the path that was already right, and its out-of-range cases are 2048 and 0, never 122..125.

Mutants run against the new tests: revert → the encode test fails; tighten decode() as well → the decode test fails; MAX_WRITE_COUNT = 125 → the encode test fails. Full suite 1968 → 1970 passing, check_ci.sh steps clean (codespell, ruff check/format, pylint 10.00, zuban).

AI assistance was used in preparing this change.

ReadWriteMultipleRegistersRequest.encode() checked the write count against
MAX_READ_COUNT (125) rather than MAX_WRITE_COUNT (121), so the client could
encode a request larger than the 253 byte MODBUS PDU maximum. MAX_WRITE_COUNT
had no other reference in the repo.

decode() keeps MAX_READ_COUNT on purpose: raising there makes DecodePDU.decode()
drop the frame, so the server would answer nothing instead of ILLEGAL_VALUE.
@dylanpulver

Copy link
Copy Markdown
Contributor Author

While checking that this was the only place a count limit disagrees with the PDU size, I found two more in the same family. Both are behaviour changes rather than a slipped constant, so I left them out of this PR — happy to add either here or in a separate one, whichever you prefer.

Measured on 3.15.0, PDU size and the RTU frame FramerRTU.buildFrame actually produces:

WriteMultipleCoilsRequest.MAX_COUNT = 2000 (FC15). PDU is 6 + ceil(N/8), so N=2000 is a 256 byte PDU / 259 byte RTU ADU, past the 253/256 maxima. 2000 is right for ReadCoilsRequest, where the response is 2 + ceil(N/8); for the write it should be 0x07B0 (1968), which is 252 bytes. #2997 only renamed this one — verifyCount(2000) was already there.

N 1968 1976 2000
PDU 252 253 256

WriteMultipleRegistersRequest.encode() (FC16) has no count check at all. PDU is 6 + 2N, so N=124 is already 254 bytes, and N>=128 fails inside struct.pack because byte_count no longer fits in a byte. datastore_update already enforces 1 <= count <= 0x07B on the server side, so the client can build a request this library's own server rejects — the same shape as the fix above.

The FC15 change would start rejecting 1969..2000, which currently works against permissive devices, so it is your call rather than mine.

@janiversen janiversen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch (copy/paste is nice, but a bit of a devil).

Thanks.

@janiversen

Copy link
Copy Markdown
Collaborator

Please add the others in another PR or 2 to make it easy to see what happens in each PR.

@janiversen
janiversen merged commit e998bde into pymodbus-dev:dev Sep 1, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants