1. Summary
A Modbus server built on nanoMODBUS can be memory-corrupted and its control flow hijacked by a single unauthenticated Modbus/TCP (or RTU) Read File Record (function code 0x14) request. The response builder in handle_read_file_record accumulates per-sub-request record data into the fixed 260-byte msg.buf with no aggregate size cap, writing up to ~8.7 KB past the buffer. Because msg.buf is the first member of struct nmbs_t, the linear overflow runs straight into the immediately-following callbacks (a table of function pointers, including read_file_record) and platform (read/write function pointers). The response loop re-reads and invokes nmbs->callbacks.read_file_record on later iterations, so an earlier iteration's overwrite of that pointer is called during the same request — a control-flow-hijack primitive, not merely a denial of service.
2. Affected Version & Scope
- Repository / product: nanoMODBUS —
https://github.com/debevv/nanoMODBUS
- Pinned commit / firmware build:
91d6782 (2026-02-01)
- Affected range (if known): HEAD
91d6782 and earlier; not fixed at HEAD (verify full range on disclosure).
- Build/config preconditions: Server role with a
read_file_record callback registered (any device that advertises FC 0x14). No non-default flags required. Bug is reachable in both TCP and RTU transports.
Technical Details & Root Cause
nanomodbus.c, handle_read_file_record:
// :1330 request_size = get_1(); // attacker byte, <=245
// subreq_count = request_size / 7; // up to 35
// :1350 uint8_t response_data_size = 0; // <-- 8-bit accumulator, WRAPS
// :1358 response_data_size += 2 + (record_length * 2); // per sub-request
// :1381 if (record_length > 124) return exception(...); // ONLY size guard: per-sub-request, no aggregate cap
// :1395-1411 response loop:
// put_1(subreq_data_size + 1);
// put_1(0x06);
// uint16_t* subreq_data = (uint16_t*)get_n(record_length * 2); // get_n advances buf_idx unchecked
// nmbs->callbacks.read_file_record(file, record, subreq_data, record_length, unit, arg);
// get_n (:100): { uint8_t* p = msg.buf + msg.buf_idx; msg.buf_idx += n; return p; } // NO bounds check
Data flow: attacker byte request_size → subreq_count → per-sub-request record_length (≤124, attacker-chosen) → cumulative response bytes N * (2 + record_length*2) = N * 250. With N sub-requests the write is N*250 bytes into a 260-byte buffer: even 2 sub-requests (500 bytes) overflow; 35 sub-requests write ~8.7 KB past msg.buf. get_n returns a pointer past msg.buf[260], and both put_1 and the callback write there.
Why it is control-flow hijack (struct nmbs_t layout, nanomodbus.h):
struct nmbs_t {
struct { uint8_t buf[260]; uint16_t buf_idx; ... } msg; // buf at offset 0
nmbs_callbacks callbacks; // <-- FUNCTION POINTERS (incl. read_file_record) right after msg
...
nmbs_platform_conf platform; // <-- read()/write() FUNCTION POINTERS
...
};
The linear OOB write runs from msg.buf into callbacks. The response loop re-reads nmbs->callbacks.read_file_record every iteration, so once an earlier iteration overwrites that pointer, a later iteration calls the overwritten value.
Root cause: missing aggregate bounds check on the response size, compounded by (a) an 8-bit response_data_size accumulator that wraps, and (b) get_n/put_1 performing no bounds check against sizeof(msg.buf).
1. Summary
A Modbus server built on nanoMODBUS can be memory-corrupted and its control flow hijacked by a single unauthenticated Modbus/TCP (or RTU) Read File Record (function code 0x14) request. The response builder in
handle_read_file_recordaccumulates per-sub-request record data into the fixed 260-bytemsg.bufwith no aggregate size cap, writing up to ~8.7 KB past the buffer. Becausemsg.bufis the first member ofstruct nmbs_t, the linear overflow runs straight into the immediately-followingcallbacks(a table of function pointers, includingread_file_record) andplatform(read/write function pointers). The response loop re-reads and invokesnmbs->callbacks.read_file_recordon later iterations, so an earlier iteration's overwrite of that pointer is called during the same request — a control-flow-hijack primitive, not merely a denial of service.2. Affected Version & Scope
https://github.com/debevv/nanoMODBUS91d6782(2026-02-01)91d6782and earlier; not fixed at HEAD (verify full range on disclosure).read_file_recordcallback registered (any device that advertises FC 0x14). No non-default flags required. Bug is reachable in both TCP and RTU transports.Technical Details & Root Cause
nanomodbus.c,handle_read_file_record:Data flow: attacker byte
request_size→subreq_count→ per-sub-requestrecord_length(≤124, attacker-chosen) → cumulative response bytesN * (2 + record_length*2)=N * 250. With N sub-requests the write isN*250bytes into a 260-byte buffer: even 2 sub-requests (500 bytes) overflow; 35 sub-requests write ~8.7 KB pastmsg.buf.get_nreturns a pointer pastmsg.buf[260], and bothput_1and the callback write there.Why it is control-flow hijack (
struct nmbs_tlayout,nanomodbus.h):The linear OOB write runs from
msg.bufintocallbacks. The response loop re-readsnmbs->callbacks.read_file_recordevery iteration, so once an earlier iteration overwrites that pointer, a later iteration calls the overwritten value.Root cause: missing aggregate bounds check on the response size, compounded by (a) an 8-bit
response_data_sizeaccumulator that wraps, and (b)get_n/put_1performing no bounds check againstsizeof(msg.buf).