Files
lmxopcua/code-reviews/Driver.Modbus.Cli/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

10 KiB

Code Review — Driver.Modbus.Cli

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

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.Cli-001, Driver.Modbus.Cli-002, Driver.Modbus.Cli-003
2 OtOpcUa conventions No issues found
3 Concurrency & thread safety Driver.Modbus.Cli-004
4 Error handling & resilience Driver.Modbus.Cli-005, Driver.Modbus.Cli-006
5 Security No issues found
6 Performance & resource management No issues found
7 Design-document adherence Driver.Modbus.Cli-007
8 Code organization & conventions No issues found
9 Testing coverage Driver.Modbus.Cli-008
10 Documentation & comments No issues found

Findings

Driver.Modbus.Cli-001

Field Value
Severity Medium
Category Correctness & logic bugs
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/SubscribeCommand.cs:43-51
Status Open

Description: SubscribeCommand synthesises its ModbusTagDefinition with only Name, Region, Address, DataType, Writable, and ByteOrder — it never exposes or passes --bit-index, --string-length, or --string-byte-order. A user running subscribe -t BitInRegister always watches bit 0 regardless of intent, and subscribe -t String runs with StringLength = 0. The doc (docs/Driver.Modbus.Cli.md) lists BitInRegister, String, Bcd16, Bcd32 in the subscribe --type help text, so these types are advertised as supported but cannot be used correctly. read and write both expose all three flags; subscribe is the odd one out.

Recommendation: Add --bit-index, --string-length, and --string-byte-order options to SubscribeCommand (mirroring ReadCommand) and pass them into the ModbusTagDefinition, or trim the --type help text to the types subscribe actually supports and reject BitInRegister / String at command entry with a clear message.

Resolution: (open)

Driver.Modbus.Cli-002

Field Value
Severity Medium
Category Correctness & logic bugs
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/WriteCommand.cs:54-89
Status Open

Description: WriteCommand rejects read-only regions (DiscreteInputs / InputRegisters) but does not validate that --type is meaningful for the Coils region. write -r Coils -a 5 -t UInt16 -v 42 builds a Coils tag with DataType = UInt16; the value parses to a boxed ushort, and the driver's WriteOneAsync coil branch calls Convert.ToBoolean(value) which succeeds for any non-zero ushort (yields true). The write silently lands as a coil ON with no diagnostic, even though the operator asked for a 16-bit register write. A coil region only supports Bool-style boolean values.

Recommendation: After the read-only-region check, reject Region == Coils combined with any non-boolean --type (anything other than Bool), with a message explaining coils carry a single bit.

Resolution: (open)

Driver.Modbus.Cli-003

Field Value
Severity Low
Category Correctness & logic bugs
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ModbusCommandBase.cs:14-24
Status Open

Description: Port (int) and TimeoutMs (int) accept any 32-bit value, including negatives and ports above 65535. UnitId is a byte, so it accepts 0-255 even though the option description and docs/Driver.Modbus.Cli.md both say the valid range is 1-247 (0 is the Modbus broadcast address; 248-255 are reserved). A negative --timeout-ms becomes a negative TimeSpan passed straight into the driver; an out-of-range --port fails later with an opaque socket error. None of these are validated at parse time.

Recommendation: Validate Port (1-65535), TimeoutMs (greater than 0), and UnitId (1-247) at the top of each command's ExecuteAsync (or in ModbusCommandBase), throwing CliFx.Exceptions.CommandException with a clear message — consistent with how WriteCommand already rejects bad regions and boolean strings.

Resolution: (open)

Driver.Modbus.Cli-004

Field Value
Severity Low
Category Concurrency & thread safety
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/SubscribeCommand.cs:61-67
Status Open

Description: The OnDataChange handler is invoked from the driver's PollGroupEngine background thread and calls console.Output.WriteLine synchronously. An exception thrown inside this handler (e.g. an IOException on a redirected or closed stdout) propagates on the poll-engine thread and is not caught — it could fault the background loop. For a long-running subscribe this is a real, if low-probability, crash path. Output lines are also written without any synchronization, so overlapping poll ticks could interleave partial lines.

Recommendation: Wrap the handler body in a try/catch that swallows or logs write failures so a transient console-write error cannot tear down the poll loop. A single lock around the write also removes the interleave risk.

Resolution: (open)

Driver.Modbus.Cli-005

Field Value
Severity Low
Category Error handling & resilience
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ProbeCommand.cs:21-54; Commands/ReadCommand.cs:46-75; Commands/WriteCommand.cs:54-89
Status Open

Description: All three commands call ConfigureLogging() then console.RegisterCancellationHandler(), but if the operator presses Ctrl+C before InitializeAsync completes, the resulting OperationCancelledException propagates out of ExecuteAsync unhandled. CliFx renders unhandled non- CommandException exceptions as a full stack trace, which is noisy for what is just a user-cancelled run. SubscribeCommand correctly catches OperationCancelledException around its Task.Delay, but the connect/read/write commands do not catch it around their driver calls.

Recommendation: Either let cancellation surface a clean message (catch OperationCancelledException in each command and exit quietly) or document that the noisy trace on Ctrl+C-during-connect is acceptable. Consistency with SubscribeCommand's handling is the cleaner choice.

Resolution: (open)

Driver.Modbus.Cli-006

Field Value
Severity Low
Category Error handling & resilience
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ProbeCommand.cs:35-53
Status Open

Description: probe reports Health: {health.State} from GetHealth(). After a successful InitializeAsync the driver sets state to Healthy regardless of whether the subsequent probe register read returns Good or a Bad status code. ReadAsync does not throw on a Modbus exception response — it returns a DataValueSnapshot with a Bad StatusCode. So probe against a host that accepts the TCP connection but rejects FC03 at the probe address prints Health: Healthy while the snapshot line below shows a Bad status. The two lines disagree, and the headline Health value (the thing an operator scans first) overstates success. The doc bills probe as the "is the PLC up + talking Modbus" check, which the bare Healthy does not actually confirm.

Recommendation: Have probe derive its headline verdict from the probe snapshot's StatusCode (Good vs Bad) rather than — or in addition to — the driver State, or print a single combined verdict line so the two cannot contradict each other.

Resolution: (open)

Driver.Modbus.Cli-007

Field Value
Severity Low
Category Design-document adherence
Location docs/Driver.Modbus.Cli.md:124-156; src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ReadCommand.cs
Status Open

Description: docs/Driver.Modbus.Cli.md devotes a whole "v2 addressing grammar" section to the industry-standard tag-address strings (40001:F:CDAB, HR1:I, C100, V2000:F:CDAB, etc.) and says "set the per-tag addressString field instead of the structured region + address + dataType fields." None of the CLI commands expose an --address-string (or equivalent) flag — read, write, and subscribe only accept the structured --region + --address + --type triple. The documented address-string grammar is reachable only through a hand-written DriverConfig JSON, not through this CLI. The doc reads as if the CLI supports it.

Recommendation: Either add an --address-string option that feeds the driver's address-string parser (and --family for the DL205/MELSEC native syntax), or scope the "v2 addressing grammar" section of the doc to note it applies to DriverConfig JSON and is not a CLI flag.

Resolution: (open)

Driver.Modbus.Cli-008

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

Description: The test project covers only the two pure-function seams: ReadCommand.SynthesiseTagName and WriteCommand.ParseValue. There is no coverage for WriteCommand's read-only-region rejection (Region is not (Coils or HoldingRegisters)), no test for ModbusCommandBase.BuildOptions (e.g. that Probe.Enabled is false and AutoReconnect tracks --disable-reconnect), and no test asserting unsupported write types throw. The branch logic in WriteCommand.ExecuteAsync and ModbusCommandBase.BuildOptions is the part most likely to regress and is currently untested. The validation gaps in findings 002/003 are also untested precisely because no test exercises that path.

Recommendation: Add tests for WriteCommand's region-validation branch and for ModbusCommandBase.BuildOptions (construct a command instance via the init setters and assert the produced ModbusDriverOptions). Once findings 002/003 are fixed, add tests for the new validation paths.

Resolution: (open)