nc_program_manager/.paul/phases/13-robustness-fixes/13-01-PLAN.md
dtrentin 32f68c5ed5 feat(14-test-hardening): env-var hardware test scaffolding + LicenseFile edge cases
Phase 14 complete — v0.4 Robustness milestone closed:
- HardwareTestHelper.GetIpOrSkip: Assert.Ignore when HEIDENHAIN/SIEMENS/MITSUBISHI_IP not set (ISS-007)
- Hardware_ConnectAsync_RealMachine [Category("Hardware")] added to all 3 integration test files
- LicenseFileTests: 4 tests covering empty, whitespace, valid base64, nonexistent path
- Validate_EmptyFingerprint_ReturnsFalse: confirms validator safe against empty input (ISS-008 scaffold)
- NcProgramManager.Tests.csproj: <Compile> entries for 2 new files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:24:58 +02:00

6.8 KiB

phase plan type wave depends_on files_modified autonomous
13-robustness-fixes 01 execute 1
Licensing/MachineFingerprint.cs
Licensing/LicenseValidator.cs
Cnc/Heidenhain/Lsv2Client.cs
true
## Goal Fix three targeted robustness defects: null dereference in WMI fingerprinting (ISS-001), undisposed SHA256 crypto provider (ISS-005), and LSV2 connect timeout that leaves a hung socket task (ISS-006).

Purpose

Shipping software with an unguarded null dereference in the license check means customers on unusual hardware get an unhandled exception with no diagnostic. The crypto leak is minor but correctness matters in security-sensitive code. The Lsv2Client timeout bug means a Heidenhain machine that is unreachable stalls the process indefinitely.

Output

Three modified source files. No new files. No interface changes.

@.paul/PROJECT.md @.paul/ISSUES.md @Licensing/MachineFingerprint.cs @Licensing/LicenseValidator.cs @Cnc/Heidenhain/Lsv2Client.cs

<acceptance_criteria>

AC-1: MachineFingerprint null guard

Given a Windows machine where a WMI property (processorID or SerialNumber) returns null
When GetFingerprint() is called
Then it returns a non-null string (partial or "unknown") instead of throwing NullReferenceException

AC-2: SHA256 provider disposed

Given LicenseValidator.Validate() is called with any input
When the method completes (success or failure)
Then the SHA256CryptoServiceProvider created during verification has been disposed

AC-3: LSV2 connect timeout aborts socket

Given a Heidenhain machine IP that is unreachable (no TCP response)
When Lsv2Client.Connect() is called and the ConnectTimeout elapses
Then Connect() returns false AND the pending TcpClient socket is closed (no lingering thread-pool task)

</acceptance_criteria>

Task 1: Null-guard WMI property reads in MachineFingerprint Licensing/MachineFingerprint.cs In GetFingerprint(), replace both unguarded property accesses with null-safe equivalents:
Line 16 — replace:
  pcInfo = mo.Properties["processorID"].Value.ToString();
with:
  pcInfo = mo.Properties["processorID"]?.Value?.ToString() ?? string.Empty;

Line 26 — replace:
  pcInfo += obj["SerialNumber"].ToString();
with:
  pcInfo += obj["SerialNumber"]?.ToString() ?? string.Empty;

Do NOT change the method signature, interface, or logic flow. Only the two property reads.
Do NOT add logging or throw — empty string fallback is intentional (fingerprint may be partial but won't crash).
Read the modified file and confirm both lines use ?. and ?? string.Empty AC-1 satisfied: no unguarded .ToString() calls on WMI properties Task 2: Wrap SHA256CryptoServiceProvider in using block Licensing/LicenseValidator.cs In Validate(), the inner rsa using block currently does: return rsa.VerifyData(data, new SHA256CryptoServiceProvider(), signature);
Replace with a nested using so SHA256 is disposed:
  using (var sha256 = new SHA256CryptoServiceProvider())
  {
      return rsa.VerifyData(data, sha256, signature);
  }

The outer `using (var rsa = new RSACryptoServiceProvider())` remains unchanged.
The enclosing try/catch remains unchanged.
Do NOT change the public key constant, constructor, or method signature.
Do NOT switch to HashAlgorithmName — that requires a different VerifyData overload not available on .NET 4.7.2 RSACryptoServiceProvider cleanly. Keep the existing overload.
Read the modified file and confirm SHA256CryptoServiceProvider is inside a using statement AC-2 satisfied: SHA256CryptoServiceProvider disposed after every Validate() call Task 3: Close TcpClient on connect timeout in Lsv2Client Cnc/Heidenhain/Lsv2Client.cs In Connect(), the current code: if (!_tcp.ConnectAsync(_config.IpAddress, _config.Port).Wait(_config.ConnectTimeout)) return false;
The bug: .Wait(timeout) returns false but leaves the pending ConnectAsync task running,
holding a thread-pool slot. Fix by closing _tcp on timeout to abort the socket:

Replace those two lines with:
  var connectTask = _tcp.ConnectAsync(_config.IpAddress, _config.Port);
  if (!connectTask.Wait((int)_config.ConnectTimeout.TotalMilliseconds))
  {
      try { _tcp.Close(); } catch { }
      return false;
  }

The `_tcp.Close()` call disposes the underlying socket, causing the pending Task to
complete with a SocketException/ObjectDisposedException rather than hanging forever.

Do NOT change anything else in Connect() or any other method.
Do NOT add async/await — Connect() is synchronous by design (.NET 4.7.2, called via GetAwaiter().GetResult() in HeidenhainMachine).
Do NOT touch Disconnect(), Login(), SendCommand(), ReadFrame(), or Crc16().
Read the modified Connect() method and confirm: (a) connectTask is assigned, (b) _tcp.Close() is called inside the timeout branch AC-3 satisfied: timeout branch closes the socket before returning false

DO NOT CHANGE

  • Licensing/LicenseValidator.cs public key constant (line 10) — vendor key, must not be modified
  • Licensing/IMachineFingerprint.cs — interface unchanged
  • Licensing/ILicenseValidator.cs — interface unchanged
  • Cnc/Heidenhain/HeidenhainMachine.cs — not in scope
  • Cnc/Heidenhain/ILsv2Client.cs — interface unchanged
  • Any test files — Phase 14 handles test changes

SCOPE LIMITS

  • No new dependencies or NuGet packages
  • No async/await refactor (ISS-002 is deferred to a future phase)
  • No logging framework (ISS-003/ISS-004 deferred)
  • No changes to any other machine implementations (Fanuc, Siemens, Mitsubishi)
  • No changes to RSA key, licensing logic, or fingerprint algorithm — only defensive null guards
Before declaring plan complete: - [ ] `Licensing/MachineFingerprint.cs` — both WMI reads use `?.` and `?? string.Empty` - [ ] `Licensing/LicenseValidator.cs` — SHA256CryptoServiceProvider in using statement - [ ] `Cnc/Heidenhain/Lsv2Client.cs` — Connect() calls `_tcp.Close()` in the timeout branch - [ ] No other files modified - [ ] No interface signatures changed - [ ] Build not broken (no new compile errors introduced)

<success_criteria>

  • All 3 tasks completed
  • All 3 verification checks pass
  • ISS-001, ISS-005, ISS-006 addressed
  • No regressions in existing code paths </success_criteria>
After completion, create `.paul/phases/13-robustness-fixes/13-01-SUMMARY.md`