Modbus File Record Transfers: Validate Subrequests, Lengths and Device Support
Functions 20 and 21 operate on file-record references, with nested request groups and their own length accounting. They are not ordinary holding-register reads and writes with a different function byte. A gateway should validate the entire structure, the expected response budget and the device’s supported file map before sending a request.
This guide is scoped to Modbus Application Protocol V1.1b3, dated 26 April 2012, specifically sections 6.14 and 6.15. It describes a parser and qualification strategy, not a tested device driver. Support, accessible files, writable regions and persistence behavior remain device-specific. Do not exercise write tests against production data or actuators.
Preserve the file-record addressing contract
The official V1.1b3 specification defines Read File Record as decimal function 20, hexadecimal 0x14, and Write File Record as decimal 21, hexadecimal 0x15. A subrequest identifies reference type 6, a file number, a starting record number and a record length. Length counts 16-bit words; the enclosing data length counts bytes.
The documented file-number range is 1 through 0xFFFF, and record numbers span 0 through 0x270F. Those protocol ranges do not prove that a product implements every file or record. Establish the actual file map and supported extents from the device documentation before constructing even a read.
Keep the file number in your internal operation model. Flattening a file/record pair into an ordinary register number loses information unless the device explicitly defines that mapping. Likewise, a “file” here does not imply a filesystem pathname, downloadable document or access to arbitrary device storage.

Validate requests in two passes
For a read, each subrequest has a seven-byte descriptor. For a write, that descriptor is followed by twice the declared word count in data bytes. The specification limits the Modbus PDU to 253 bytes and gives separate request data-length bounds: 0x07–0xF5 for reads and 0x09–0xFB for writes. Apply all relevant limits rather than assuming the transport envelope is the only constraint.
Use the first pass to establish structural safety. Check the actual PDU length against the enclosing count, then walk every complete descriptor without reading past the buffer. For writes, verify the declared payload exists before advancing. Reject trailing incomplete descriptors and unconsumed bytes. Perform size arithmetic in a type that cannot overflow for the admitted input.
Use the second pass for semantic admission: reference type, known file, supported starting record, quantity and end position. Validate the whole request before dispatch so an invalid later subrequest cannot accidentally allow an earlier write. This is a gateway design recommendation; it does not promise transaction atomicity in a remote device.
Budget the read response before accepting the request
A small read request can ask for a large response. Calculate the response requirement from every requested quantity, including each subresponse’s length byte and reference-type byte. Compare that budget with the protocol envelope, the device’s documented limit and your own receive-buffer policy.
Do not accept an oversized operation merely because the request fits. If the gateway splits an operation into several requests, make that behavior explicit: the observations may occur at different times, and a failure can leave only some results available. A caller that needs one coherent device snapshot requires a device-supported mechanism, not an invisible packet split.
Use an original length ledger as a parser fixture
Consider a hypothetical read with two groups: three words from file 3 starting at record 5, and two words from file 7 starting at record 30. These files and records are invented for an offline fixture and do not identify any real device.
- The request contains two seven-byte descriptors, so its data count is 14 bytes and its PDU is 16 bytes including function and count.
- The first read subresponse has a length value of 7: one reference-type byte plus six data bytes.
- The second subresponse has a length value of 5: one reference-type byte plus four data bytes.
- Including both subresponse length bytes, the response data count is 14 and its PDU is 16 bytes.
- A write carrying the same two groups contains 24 data bytes after function and count, producing a 26-byte PDU.
These fixture lengths were checked with local Python byte construction; no network request was sent. They do not validate a complete production parser.
One documentation trap deserves attention: the V1.1b3 read-response table prints lower bounds that conflict with its own two-register example, whose subresponse length is 0x05. Do not reject that layout solely by copying the table’s printed lower bound. Derive nested lengths from the described structure, compare them with the specification’s example, and qualify the interpretation with the selected peer.
Match the response to the outstanding operation
For reads, walk the outer count and every subresponse independently. Require the expected number and order of groups, the proper reference type and exactly the data length implied by each requested quantity. A normal read subresponse does not repeat the full file/record address, so retain the outstanding request context rather than trying to infer it from returned data.
For writes, V1.1b3 defines the normal response as an echo of the request. Compare it with the admitted request, including all groups and values. Keep exception responses distinct from normal structured responses. A valid echo establishes the protocol response; any stronger claim about durable storage or coordinated multi-group updates needs the device’s contract and additional tests.
If a write times out, classify the outcome as uncertain until the approved reconciliation procedure establishes what happened. Do not turn a transport retry into a claim that the original operation never reached the device.
Test failures at each nesting boundary
Build an offline test corpus before connecting hardware. Include a truncated descriptor, inconsistent outer count, wrong reference type, missing write data, excess trailing bytes, unknown file, an end position beyond the supported extent, and a request whose expected response exceeds the admitted budget. Each should fail at a named validation stage.
For response tests, include missing and extra groups, an incorrect nested length, an unexpected reference type and an echo with one changed word. Verify that no partial decode is reported as a successful full transfer. Then test valid minimum and maximum operations against a simulator and the exact device firmware, keeping those results separate.
For hardware write qualification, use a documented disposable area and an explicit restoration plan. Confirm readback and persistence only to the depth the product requires. The release record should state which file ranges and grouped operations were tested, what remains unsupported, and how uncertain outcomes are handled. That is a defensible file-record contract rather than a generic claim of Modbus compatibility.