Environment
- python-snap7 3.1.2 (PyPI) and current
master (4e960e5) — same behaviour on both
- Pure-Python server (
snap7.server.Server), macOS 26.6.2 arm64, CPython 3.12
- Reference implementation for comparison: python-snap7 2.1.0 (native C++ Snap7 server)
Summary
When a Read Var request targets an area that was never passed to register_area(),
or an offset beyond the end of a registered area, the pure-Python server answers with
item return code 0xFF (success) and fabricated data instead of a CPU error code.
A real S7 CPU — and the C++ Snap7 server this library is named after — answers
0x0A (Item not available) and 0x05 (Address out of range) respectively.
Because the server's stated purpose is "testing and simulation", this silently turns
negative test cases into passing ones. A test that reads the wrong DB number, or an
offset past the end of a DB, gets plausible-looking bytes and a success code.
Observed
Using the library's own client against the pure-Python server, with only DB1
(32 bytes) registered:
| Request |
Result |
db_read(404, 0, 2) — unregistered DB |
returns 42ff, no error |
db_read(404, 0, 4) — unregistered DB |
returns 42ff1234, no error |
db_read(404, 0, 20) — unregistered DB |
raises S7ProtocolError: Read response size mismatch: expected 20 bytes, received 4 |
db_read(1, 100, 4) — offset past end of a 32-byte DB |
returns 00000000, no error |
db_read(1, 30, 8) — read crossing the end of the DB |
returns 0000000000000000, no error |
Note the third row: the fabricated buffer is only 4 bytes long, so any request for
more than 4 bytes gets a short read carrying a success return code. Clients that
trust the return code and copy requested_size bytes out of the response read past
the payload.
Reference behaviour (C++ Snap7 server, python-snap7 2.1.0)
Same five requests, same registered areas, raw item return codes taken off the wire:
| Request |
Pure-Python server |
C++ Snap7 server |
| unregistered DB, 2 bytes |
0xFF + 42ff |
0x0A Item not available |
| unregistered DB, 20 bytes |
0xFF + 4 bytes |
0x0A Item not available |
| registered DB, offset past end |
0xFF + zeros |
0x05 Address out of range |
| registered DB, read crossing the end |
0xFF + zero-padded |
0x05 Address out of range |
Reproducer
# server.py
import struct, time
from snap7.server import Server
from snap7.type import SrvArea
db1 = bytearray(32)
struct.pack_into(">f", db1, 0, 3.14)
srv = Server()
srv.register_area(SrvArea.DB, 1, db1)
srv.start(tcp_port=11199)
print("READY", flush=True)
while True:
time.sleep(1)
# client.py
import snap7
c = snap7.client.Client()
c.connect("127.0.0.1", 0, 1, tcp_port=11199)
print(bytes(c.db_read(404, 0, 4)).hex()) # unregistered DB -> 42ff1234, expected an error
print(bytes(c.db_read(1, 100, 4)).hex()) # past end of DB1 -> 00000000, expected an error
c.disconnect()
Where
snap7/server/__init__.py, _read_from_memory_area() (master, around L1044):
if area_key not in self.memory_areas:
logger.warning(f"Memory area {area}#{db_number} not registered")
# Return dummy data if area not found (for compatibility)
return bytearray([0x42, 0xFF, 0x12, 0x34])[:count]
...
if start >= len(area_data):
logger.warning(f"Start address {start} beyond area size {len(area_data)}")
return bytearray([0x00] * count)
...
# Pad with zeros if we didn't read enough
if len(read_data) < count:
read_data.extend([0x00] * (count - len(read_data)))
_handle_read_area() only builds an error response when _read_from_memory_area()
returns None, which none of these three paths do, so the item is always emitted as
struct.pack(">BBH", 0xFF, 0x04, len(read_data) * 8) + read_data.
Suggested fix
Have _read_from_memory_area() distinguish the three outcomes and let
_handle_read_area() emit the matching item return code:
- area not registered →
0x0A
start >= len(area_data) → 0x05
start + count > len(area_data) → 0x05
The "(for compatibility)" comment suggests the dummy data was deliberate at some
point; if some caller depends on it, an opt-in flag on Server(...) would keep that
working while making the default behave like a CPU.
Happy to send a PR if the approach looks right.
Not a duplicate of #889
#889 / #893 are about the Connection Confirm framing. This one is on the Read Var
path and is independent of how the connection is established — the reproducer above
uses the library's own client, which connects fine.
Environment
master(4e960e5) — same behaviour on bothsnap7.server.Server), macOS 26.6.2 arm64, CPython 3.12Summary
When a Read Var request targets an area that was never passed to
register_area(),or an offset beyond the end of a registered area, the pure-Python server answers with
item return code
0xFF(success) and fabricated data instead of a CPU error code.A real S7 CPU — and the C++ Snap7 server this library is named after — answers
0x0A(Item not available) and0x05(Address out of range) respectively.Because the server's stated purpose is "testing and simulation", this silently turns
negative test cases into passing ones. A test that reads the wrong DB number, or an
offset past the end of a DB, gets plausible-looking bytes and a success code.
Observed
Using the library's own client against the pure-Python server, with only
DB1(32 bytes) registered:
db_read(404, 0, 2)— unregistered DB42ff, no errordb_read(404, 0, 4)— unregistered DB42ff1234, no errordb_read(404, 0, 20)— unregistered DBS7ProtocolError: Read response size mismatch: expected 20 bytes, received 4db_read(1, 100, 4)— offset past end of a 32-byte DB00000000, no errordb_read(1, 30, 8)— read crossing the end of the DB0000000000000000, no errorNote the third row: the fabricated buffer is only 4 bytes long, so any request for
more than 4 bytes gets a short read carrying a success return code. Clients that
trust the return code and copy
requested_sizebytes out of the response read pastthe payload.
Reference behaviour (C++ Snap7 server, python-snap7 2.1.0)
Same five requests, same registered areas, raw item return codes taken off the wire:
0xFF+42ff0x0AItem not available0xFF+ 4 bytes0x0AItem not available0xFF+ zeros0x05Address out of range0xFF+ zero-padded0x05Address out of rangeReproducer
Where
snap7/server/__init__.py,_read_from_memory_area()(master, around L1044):_handle_read_area()only builds an error response when_read_from_memory_area()returns
None, which none of these three paths do, so the item is always emitted asstruct.pack(">BBH", 0xFF, 0x04, len(read_data) * 8) + read_data.Suggested fix
Have
_read_from_memory_area()distinguish the three outcomes and let_handle_read_area()emit the matching item return code:0x0Astart >= len(area_data)→0x05start + count > len(area_data)→0x05The "(for compatibility)" comment suggests the dummy data was deliberate at some
point; if some caller depends on it, an opt-in flag on
Server(...)would keep thatworking while making the default behave like a CPU.
Happy to send a PR if the approach looks right.
Not a duplicate of #889
#889 / #893 are about the Connection Confirm framing. This one is on the Read Var
path and is independent of how the connection is established — the reproducer above
uses the library's own client, which connects fine.