Skip to content

Pure-Python server: reads of unregistered areas and out-of-bounds offsets return success with fabricated data instead of a CPU error code #894

Description

@yuta2

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions