Files
lmxopcua/code-reviews/Driver.Modbus.Addressing/findings.md
Joseph Doherty 8568f5cd85 docs(code-reviews): comprehensive per-module review pass at 76d35d1
Reviewed all 31 src/ production projects against the 10-category
checklist in REVIEW-PROCESS.md. Each module gets its own findings.md;
code-reviews/README.md is regenerated from them.

334 findings: 6 Critical, 46 High, 126 Medium, 156 Low.

Critical findings:
- Server-001: WriteNodeIdUnknown recurses unconditionally — a HistoryRead
  on an unresolvable node crashes the process (remote DoS).
- Admin-001/002: app-wide auth bypass (RouteView not AuthorizeRouteView)
  plus unauthenticated mutating routes.
- Core.Scripting-001: System.Environment reachable from operator scripts;
  Environment.Exit() terminates the server.
- Core.AlarmHistorian-001: rowIds/events parallel-list desync on a corrupt
  payload misapplies outcomes — silent alarm-event data loss.
- Driver.Galaxy-001: ReconnectSupervisor is built but never triggered, so
  a transient gateway drop permanently kills the event stream.

All findings are Status=Open; resolution is tracked per REVIEW-PROCESS.md
section 4. Review only — no source code changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 05:20:27 -04:00

11 KiB

Code Review — Driver.Modbus.Addressing

Field Value
Module src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing
Reviewer Claude Code
Review date 2026-05-22
Commit reviewed 76d35d1
Status Reviewed
Open findings 9

Checklist coverage

A comprehensive review completes every category, recording "No issues found" where a category produced nothing rather than leaving it blank.

# Category Result
1 Correctness & logic bugs Driver.Modbus.Addressing-001, -002, -003, -004
2 OtOpcUa conventions No issues found
3 Concurrency & thread safety No issues found
4 Error handling & resilience Driver.Modbus.Addressing-005, -006
5 Security No issues found
6 Performance & resource management No issues found
7 Design-document adherence Driver.Modbus.Addressing-001, -007
8 Code organization & conventions No issues found
9 Testing coverage Driver.Modbus.Addressing-008
10 Documentation & comments Driver.Modbus.Addressing-009

Findings

Driver.Modbus.Addressing-001

Field Value
Severity High
Category Correctness & logic bugs
Location ModbusAddressParser.cs:230-235, DirectLogicAddress.cs:66-73
Status Open

Description: The DL205 family-native branch routes every V-prefixed address through DirectLogicAddress.UserVMemoryToPdu, which is a plain octal-to-decimal conversion. DL205/DL260 system V-memory (V40400 and up) is NOT a simple octal decode — per docs/v2/dl205.md section V-Memory, V40400 must map to Modbus PDU 0x2100 (decimal 8448) on a factory-mode ECOM module. The parser instead octal-decodes V40400 to decimal 16640 (0x4100), the wrong register. The DirectLogicAddress.SystemVMemoryToPdu / SystemVMemoryBasePdu helper that exists to do this correctly is never called by the parser — it is dead code from the parser point of view. A tag spreadsheet that addresses any DL system register through the grammar string silently reads and writes the wrong PLC memory. The companion test ModbusFamilyParserTests.cs:20 bakes the wrong value (V40400 to 16640) into a passing assertion, so the regression is locked in.

Recommendation: Make the DL205 V branch detect the system bank (octal address >= 40400) and route it through SystemVMemoryToPdu, or explicitly reject system V-memory in the grammar string with a diagnostic pointing at the structured tag form. Either way, fix the V40400 test to assert the corrected mapping.

Resolution: (open)

Driver.Modbus.Addressing-002

Field Value
Severity Medium
Category Correctness & logic bugs
Location ModbusAddressParser.cs:86-94
Status Open

Description: In the 3-field disambiguation, an empty 3rd field (40001:F:) reaches parts[2].All(char.IsDigit). Enumerable.All returns true for an empty sequence, so the empty string is classified as a valid-shaped array count, assigned to countPart, then silently dropped by the later string.IsNullOrEmpty(countPart) guard. The result is that 40001:F: parses successfully as a plain scalar with a dangling empty field rather than being rejected as malformed. The 4-field form 40001:F:: has the analogous effect. A user who mistypes a trailing colon gets no diagnostic.

Recommendation: Reject an empty 3rd field explicitly, or guard the All(char.IsDigit) branch with parts[2].Length > 0.

Resolution: (open)

Driver.Modbus.Addressing-003

Field Value
Severity Medium
Category Correctness & logic bugs
Location ModbusAddressParser.cs:405-406, ModbusAddressParser.cs:128
Status Open

Description: LooksLikeByteOrderToken classifies any 4-letter token as a byte-order token. A 3-field address whose 3rd field is a 4-letter type-like token (e.g. 40001:S:BOOL) is routed into TryParseByteOrder, producing the misleading diagnostic "Unknown byte order BOOL" instead of telling the user the type belongs in field 2. The type code BOOL is exactly 4 letters and could only ever be intended as a type — the shape heuristic cannot tell a mistyped type from a byte order, so the diagnostic actively misdirects.

Recommendation: When TryParseByteOrder fails on a 4-letter token in the 3-field form, widen the error message to mention that field 3 is a byte order and field 2 is the type, or attempt a type-parse fallback before emitting the byte-order error.

Resolution: (open)

Driver.Modbus.Addressing-004

Field Value
Severity Medium
Category Correctness & logic bugs
Location ModbusAddressParser.cs:182-194
Status Open

Description: The bit suffix is stripped using text.IndexOf('.') — the first dot. An input such as 40001.5.3 produces a bit text of "5.3", rejected by byte.TryParse with the generic "Bit index must be 0..15" message. A Modicon-style decimal-point typo like 400.01 is silently treated as region/offset 400 plus bit 01; 400 then fails Modicon length validation, so the surfaced error is the Modicon length diagnostic rather than a bit-index diagnostic, because the bit was parsed first and 01 is a valid bit. The dot-handling assumes a single dot without asserting it, and the diagnostics for these malformed inputs are inconsistent.

Recommendation: Use LastIndexOf('.') or assert exactly one dot, and validate that the region/offset segment is non-empty and dot-free after the strip so malformed inputs get a precise diagnostic.

Resolution: (open)

Driver.Modbus.Addressing-005

Field Value
Severity Medium
Category Error handling & resilience
Location ModbusAddressParser.cs:200-213
Status Open

Description: TryParseRegionAndOffset tries family-native, then mnemonic, then Modicon. When all three fail it returns false with whatever error the Modicon parser last wrote (comment: "the Modicon error is the more specific diagnostic"). For a non-Generic family this is misleading: TryParseFamilyNative returns false with error left null for any address that does not start with a recognised family prefix, and even for recognised prefixes it only sets error inside the catch. The subsequent mnemonic and Modicon attempts overwrite error. Net effect: a clearly family-native-shaped input that fails deep in the family helper can still surface a generic Modicon "must be 5 or 6 digits" error, hiding the real cause (e.g. "contains non-octal digit").

Recommendation: When a non-Generic family is configured and the input matches a family prefix, prefer and preserve the family-native error rather than letting the Modicon fallback overwrite it.

Resolution: (open)

Driver.Modbus.Addressing-006

Field Value
Severity Low
Category Error handling & resilience
Location ModbusAddressParser.cs:297-301
Status Open

Description: TryParseFamilyNative catches only ArgumentException and OverflowException. The current helpers throw only those (including ArgumentOutOfRangeException, which derives from ArgumentException), so today it is correct. But the parser intent is to convert helper exceptions into structured errors; any future helper change that throws a different exception type (e.g. a FormatException from a ushort.Parse swap) would escape as an unhandled exception out of a TryParse method, violating the try-parse contract that config-bind hot-path callers depend on.

Recommendation: Either document the exact exception contract of the helpers and keep the narrow catch, or broaden to a general catch-all that records the message — a try-parse method should never throw.

Resolution: (open)

Driver.Modbus.Addressing-007

Field Value
Severity Low
Category Design-document adherence
Location ModbusDataType.cs:91-95, docs/v2/dl205.md section Strings
Status Open

Description: ModbusStringByteOrder (HighByteFirst / LowByteFirst) is defined in this assembly and documented as the DL205 low-byte-first string-packing knob, but ParsedModbusAddress has no field for it and ModbusAddressParser never produces or consumes it. The STR<n> grammar form cannot express the DL205 string byte order described in docs/v2/dl205.md — a DL205 string tag parsed from the grammar string always carries the default order. The enum is effectively unreachable from the parser, so the grammar cannot represent a known, documented device quirk.

Recommendation: Either add a StringByteOrder field to ParsedModbusAddress plus a grammar token for it, or document explicitly that DL205 string byte order is only configurable via the structured tag form and is intentionally out of grammar scope.

Resolution: (open)

Driver.Modbus.Addressing-008

Field Value
Severity Medium
Category Testing coverage
Location tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests/
Status Open

Description: Several edge cases of the address arithmetic are untested or asserted wrong: (a) DL205 system V-memory mapping is tested only with the incorrect expected value (ModbusFamilyParserTests.cs:20, see finding -001); (b) there is no test for UserVMemoryToPdu or AddOctalOffset overflow (V200000, C200000) hitting the OverflowException path; (c) no test for the empty-trailing-field cases of finding -002; (d) MelsecAddress.ParseHex overflow and DRegisterToHolding / MRelayToCoil bank-base overflow are untested; (e) no test that SystemVMemoryToPdu is exercised at all. The address-arithmetic overflow and off-by-one paths are exactly the high-risk surface this module owns, and they are the least covered.

Recommendation: Add overflow/boundary tests for every PDU/coil/discrete translation helper and for the parser count/bit/field edge cases. Correct the V40400 assertion as part of fixing finding -001.

Resolution: (open)

Driver.Modbus.Addressing-009

Field Value
Severity Low
Category Documentation & comments
Location ModbusModiconAddress.cs:55-64, ModbusModiconAddress.cs:104-110
Status Open

Description: The comments on ModbusModiconAddress.TryParse are slightly inaccurate. The remark that 5-digit Modicon is always exactly 5 chars (40001..49999) and 6-digit is exactly 6 (400001..465536-shaped) implies the leading digit is always 4, but the parser accepts leading 0/1/3 too — a 5-digit coil is 00001..09999, not 40001..49999. Separately, the line-106 comment says the 5-digit form caps at 9999 by construction while the adjacent code path applies the same > 65536 check to both forms; the comment describes an invariant the code does not rely on.

Recommendation: Reword the range examples to cover all four region digits and drop the caps-at-9999 aside or restate it as a precise statement about trailing-digit count.

Resolution: (open)