Files
lmxopcua/code-reviews/Driver.Modbus.Contracts/findings.md
T
Joseph Doherty a7ce742763 review(Driver.Modbus.Contracts): first review; guard equipment-tag bitIndex/stringLength
First review at 7286d320. -001 (Medium): reject bitIndex>15 for BitInRegister equipment tags
(was silent always-false/no-op via the (1<<16) mask). -002 (Medium): reject stringLength<1
for String (was misleading BadCommunicationError). -003/-004 cap-doc + [Range(0,2000)] on
MaxCoilsPerRead. Consuming Driver.Modbus.Tests green.
2026-06-19 12:22:53 -04:00

6.2 KiB

Code Review — Driver.Modbus.Contracts

Field Value
Module src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts
Reviewer Claude Code
Review date 2026-06-19
Commit reviewed a19b0f86
Status Reviewed
Open findings 0

Checklist coverage

# Category Result
1 Correctness & logic bugs 2 findings (001, 002) — both fixed in this review
2 OtOpcUa conventions No issues found
3 Concurrency & thread safety No issues found
4 Error handling & resilience No issues found
5 Security No issues found
6 Performance & resource management No issues found
7 Design-document adherence No issues found
8 Code organization & conventions No issues found
9 Testing coverage No dedicated test project. The pre-declared-tag parse path is covered in Driver.Modbus.Tests; the equipment-tag TryParse path has no unit tests — deferred.
10 Documentation & comments 2 findings (003, 004) — both fixed in this review

Findings

Driver.Modbus.Contracts-001

Field Value
Severity Medium
Category Correctness & logic bugs
Location src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusEquipmentTagParser.cs:35
Status Resolved

Description: TryParse cast the JSON "bitIndex" value to byte with no range check. ModbusTagDefinition documents the valid range as 0-15 (LSB-first within a 16-bit register). When a user typed a BitIndex >= 16 in the AdminUI numeric input, the byte was passed unmodified to the driver. ModbusDriver computes (raw & (1 << tag.BitIndex)) != 0 for reads and (ushort)(current | (1 << bit)) for writes. Because C# int shifts are modulo-32, 1 << 16 = 0x10000; masking against a ushort read value always produces 0 (read always returns false) and the write OR has no effect on the register. Correct data flow is silently lost for any BitInRegister equipment tag whose bitIndex is 16 or greater. ModbusTagConfigModel.Validate() returned null unconditionally so the AdminUI save path did not gate this. (The grammar parser ModbusAddressParser.TryParse correctly rejects bit indices > 15 on line 247, but equipment tags bypass the grammar parser.)

Recommendation: Add a range guard in TryParse immediately after reading bitIndex: if (dataType == ModbusDataType.BitInRegister && bitIndex > 15) return false;

Resolution: Fixed in this review (2026-06-19). Added guard in ModbusEquipmentTagParser.TryParse after the bitIndex read. The parser now returns false (BadNodeIdUnknown at the driver) rather than silently producing a broken definition. Build verified (0 errors, 0 warnings).


Driver.Modbus.Contracts-002

Field Value
Severity Medium
Category Correctness & logic bugs
Location src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusEquipmentTagParser.cs:36
Status Resolved

Description: TryParse read "stringLength" via ReadInt (returning 0 when absent) and constructed a ModbusTagDefinition with StringLength = 0 even when DataType = String. No equivalent of ModbusDriverFactoryExtensions.ValidateStringLength was applied on the equipment-tag path. At runtime RegisterCount computes (0 + 1) / 2 = 0 (integer division), the driver issues a 0-register FC03/FC04 PDU, the PLC responds with Modbus exception 03 (Illegal Data Value), and the tag surfaces a misleading BadCommunicationError OPC UA status. ModbusTagConfigModel.Validate() returned null unconditionally, so the AdminUI save path also did not guard against this.

Recommendation: Add if (dataType == ModbusDataType.String && stringLength < 1) return false; in TryParse.

Resolution: Fixed in this review (2026-06-19). Added guard after the stringLength read, analogous to the existing Driver.Modbus-009 / ValidateStringLength protection in the pre-declared tag path. Build verified (0 errors, 0 warnings).


Driver.Modbus.Contracts-003

Field Value
Severity Low
Category Documentation & comments
Location src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusDriverOptions.cs:40
Status Resolved

Description: MaxRegistersPerRead carried the comment "Setting to 0 disables the cap (discouraged — the spec upper bound still applies)". This is inaccurate and self-contradictory: setting the value to 0 causes the ReadPlanner to substitute the Modbus-spec maximum of 125, not to remove the cap. An operator reading "disables the cap" could reasonably expect unlimited chunk sizes.

Recommendation: Replace "disables the cap" with precise language stating that 0 resets to spec maximum.

Resolution: Fixed in this review (2026-06-19). Replaced the comment with language explaining that 0 resets to the spec maximum (125) and noting that values above 125 are spec-illegal for conforming servers. Build verified.


Driver.Modbus.Contracts-004

Field Value
Severity Low
Category Documentation & comments
Location src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusDriverOptions.cs:53-60
Status Resolved

Description: MaxCoilsPerRead is typed ushort (0-65535) but the Modbus spec limits FC01/FC02 to 2000 coils per request. The comment only warned against 0; it did not warn that values above 2000 are spec-illegal. Setting MaxCoilsPerRead = 2001 caused the ReadPlanner to issue a single PDU requesting 2001 coils, which most PLCs reject with exception 03. No [Range] attribute was present to constrain Options binding, unlike ProbeTimeoutSeconds which already carries [Range(1, 60)]. The comment also used the inaccurate "disables the cap" phrasing (same root issue as 003).

Recommendation: Add [Range(0, 2000)] and clarify the 0-means-spec-maximum semantics in the doc.

Resolution: Fixed in this review (2026-06-19). Added [Range(0, 2000)] attribute to MaxCoilsPerRead and updated the comment to explain that 0 resets to spec maximum and that values above 2000 are spec-illegal. Build verified (0 errors, 0 warnings).