fix(driver-ablegacy): resolve High code-review findings (Driver.AbLegacy-001, Driver.AbLegacy-006)

Driver.AbLegacy-001 — PCCC bit-index range. AbLegacyAddress.TryParse
accepted a bit index of 0..31 for every file type, but a 16-bit
N/B/I/O/S/A word only has bits 0..15. TryParse now range-checks the
bit index against the file's word width (0..15 for 16-bit element
files, 0..31 for the 32-bit L file, no bits on float files), so
addresses like N7:0/20 are rejected at parse time instead of silently
truncating in the (short) cast. WriteBitInWordAsync reads and writes
an L-file parent word as 32-bit Long and masks the RMW arithmetic to
the native width, so a sign-extended 16-bit decode can no longer
corrupt the high bits.

Driver.AbLegacy-006 — shared-runtime concurrency. A per-tag libplctag
Tag handle is cached and reused by both the server read path and the
poll loop, with no synchronisation around Read/GetStatus/DecodeValue.
Added a per-runtime SemaphoreSlim (DeviceState.GetRuntimeLock, keyed
by tag name); ReadAsync and WriteAsync now hold it across the whole
Read -> GetStatus -> Decode / Encode -> Write -> GetStatus sequence so
no two threads touch the same Tag handle concurrently.

Added xUnit + Shouldly regression coverage: AbLegacyBitIndexRangeTests
(per-file bit-range validation + L-file 32-bit RMW + sign-extension
safety) and AbLegacyRuntimeConcurrencyTests (overlap-detecting fake
proving concurrent read/read and read/write are serialised).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 06:33:10 -04:00
parent 8a7668c678
commit d89be2a011
5 changed files with 334 additions and 20 deletions

View File

@@ -7,7 +7,7 @@
| Review date | 2026-05-22 |
| Commit reviewed | `76d35d1` |
| Status | Reviewed |
| Open findings | 13 |
| Open findings | 11 |
## Checklist coverage
@@ -36,7 +36,7 @@ a category produced nothing rather than leaving it blank.
| Severity | High |
| Category | Correctness & logic bugs |
| Location | `AbLegacyAddress.cs:54`, `AbLegacyDriver.cs:368-374` |
| Status | Open |
| Status | Resolved |
**Description:** `AbLegacyAddress.TryParse` accepts a `BitIndex` of `0..31` for every
file type. A PCCC N-file word is a signed 16-bit integer, so valid bit indices are
@@ -54,7 +54,10 @@ in `WriteBitInWordAsync` - reject bit > 15 for N/B/I/O/S files and bit > 31 for
files. For bit-in-word RMW against L files, read the parent as `Long`. Mask the
read-back value to the word width before applying the bit operation.
**Resolution:** _(open)_
**Resolution:** Resolved 2026-05-22 — `AbLegacyAddress.TryParse` now range-checks the
bit index against per-file word width (0..15 for N/B/I/O/S/A, 0..31 for L, no bits on
F); `WriteBitInWordAsync` reads/writes an L-file parent as 32-bit `Long` and masks the
RMW arithmetic to the native width so sign-extension can no longer corrupt high bits.
### Driver.AbLegacy-002
@@ -161,7 +164,7 @@ libplctag status per device.
| Severity | High |
| Category | Concurrency & thread safety |
| Location | `AbLegacyDriver.cs:107-158`, `AbLegacyDriver.cs:162-234`, `LibplctagLegacyTagRuntime.cs` |
| Status | Open |
| Status | Resolved |
**Description:** A per-tag `IAbLegacyTagRuntime` (wrapping a single libplctag `Tag`)
is cached in `DeviceState.Runtimes` and reused. `ReadAsync` (called directly by the
@@ -179,7 +182,10 @@ hazard. Only the bit-in-word RMW path is serialised (per-parent `SemaphoreSlim`)
`SemaphoreSlim`, or a per-device read lock - so no two threads touch the same `Tag`
handle concurrently.
**Resolution:** _(open)_
**Resolution:** Resolved 2026-05-22 — added a per-runtime `SemaphoreSlim`
(`DeviceState.GetRuntimeLock`, keyed by tag name); `ReadAsync` and `WriteAsync` now
hold it around the whole Read→GetStatus→Decode / Encode→Write→GetStatus sequence so the
shared libplctag `Tag` handle is never touched by two threads at once.
### Driver.AbLegacy-007