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

235 lines
10 KiB
Markdown

# 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)_