---
phase: 13-robustness-fixes
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- Licensing/MachineFingerprint.cs
- Licensing/LicenseValidator.cs
- Cnc/Heidenhain/Lsv2Client.cs
autonomous: 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
## AC-1: MachineFingerprint null guard
```gherkin
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
```gherkin
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
```gherkin
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)
```
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)
- All 3 tasks completed
- All 3 verification checks pass
- ISS-001, ISS-005, ISS-006 addressed
- No regressions in existing code paths