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>
235 lines
10 KiB
Markdown
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)_
|