---
phase: 14
slug: usb-serial-foundation-and-led-control
status: verified
threats_open: 0
asvs_level: 1
created: 2026-04-19
---

# Phase 14 — Security

> Per-phase security contract: threat register, accepted risks, and audit trail.
> Covers all three sub-plans (14-01 spike, 14-02 LED core module, 14-03 driver integration).

---

## Trust Boundaries

| Boundary | Description | Data Crossing |
|----------|-------------|---------------|
| Spike CLI argv -> CreateFileA path | User-supplied COM port name concatenated into `\\.\<name>` (spike only) | ASCII port name |
| Spike -> MagWLED-1 USB CDC | Device-controlled reply bytes consumed as debug output | Untrusted serial bytes (read by spike only) |
| VRSettings (`steamvr.vrsettings` JSON) -> DeviceProvider::Init | User-editable config values read once at driver start | `backglow_com_port` (string), `backglow_brightness_ceiling` (int32) |
| DeviceProvider / LedController -> WledSerialTransport | In-process; transport owned by unique_ptr, never reassigned | Adalight frame bytes + JSON control lines |
| WledSerialTransport -> external USB CDC device | Write-only in Phase 14 (reads deferred to Phase 15) | Adalight frames, `{"bri":N}`, `{"on":bool}` |
| Local user via `\\.\pipe\beyond_proximity_ctl` -> HandlePipeCommand | Any local process with pipe access can issue `backglow` commands | Command strings (validated in `HandleBackglowCommand`) |
| OS WM_DEVICECHANGE messages -> hotplug thread | Kernel-sourced device arrival events for COM ports | `DBT_DEVICEARRIVAL` + `DEV_BROADCAST_DEVICEINTERFACE_A` |

---

## Threat Register

### Plan 14-01 — USB-Serial Hardware Validation Spike

| Threat ID | Category | Component | Disposition | Mitigation | Status |
|-----------|----------|-----------|-------------|------------|--------|
| T-14.1-01 | Tampering | `OpenComPort` path construction in `serial_spike.cpp` | mitigate | `IsValidComPortName` at `serial_spike.cpp:67-76` requires `COM` + 1–3 digits, length 4..6, rejects path-traversal/backslash/space before `\\.\<name>` concat at `:84`. Enforced at argv parse (`:380-383`). | closed |
| T-14.1-02 | DoS | Hardware reply read loop (`'v'` round-trip) | mitigate | `COMMTIMEOUTS.ReadTotalTimeoutConstant = 10` at `serial_spike.cpp:124`; `StepV` outer wall-clock bound of 2000 ms via `std::chrono::steady_clock::now() + std::chrono::milliseconds(2000)` at `:224`; `OverlappedReadSome` bounded to 100 ms per call at `:196`. No unbounded blocking. | closed |
| T-14.1-03 | Information disclosure | Spike logs | accept | Spike is throwaway CLI run locally; logs contain COM port name + Adalight bytes + WLED version string. No secrets, no PII. See Accepted Risks log. | closed |
| T-14.1-04 | Elevation of privilege | Spike executable | accept | `backglow_serial_spike.exe` is user-space CLI with exclusive COM access; no driver/service install. See Accepted Risks log. | closed |
| T-14.1-05 | Spoofing | `'v'` reply parsing | mitigate | `StepV` at `serial_spike.cpp:239` requires reply begin with ASCII `"WLED "` (memcmp of exactly 5 bytes). Anything else logged as raw hex with FAIL disposition (`:251-255`). No trust decisions derived from reply content. | closed |

### Plan 14-02 — LED Core Module (Transport + Controller)

| Threat ID | Category | Component | Disposition | Mitigation | Status |
|-----------|----------|-----------|-------------|------------|--------|
| T-14.2-01 | Tampering | Port name passed to `WledSerialTransport::Open` | mitigate | `IsValidComName` at `wled_serial.cpp:42-51` enforces `^COM[0-9]{1,3}$` (length 4..6, `COM` prefix, digits only). On reject: `m_lastError = ERROR_INVALID_NAME (123)` at `:57` before `\\.\<name>` concat at `:63`. | closed |
| T-14.2-02 | DoS | Writer thread blocked by misbehaving hardware | mitigate | `WriteAllWithTimeout` at `wled_serial.cpp:150-202` uses `FILE_FLAG_OVERLAPPED` at `:71`, `WaitForSingleObject(ev, 100)` at `:173`, `CancelIoEx` on timeout at `:175`, `Close()` on any failure at `:170/180/187/196`. DCB `WriteTotalTimeoutConstant = 50` at `:99`. Max per-write wait bounded to ~100 ms. | closed |
| T-14.2-03 | DoS / safety | Brightness ceiling bypass | mitigate | `LedController::WriterThreadFunc` applies `ApplyCeilingInPlace` at `led_controller.cpp:166` on a **local copy** of the staged buffer **before** `m_transport->SendRgbFrame(frame, ...)` at `:168`. `SendBrightness` branch clamps `briVal` via `(briVal * ceiling) / 255u` at `:142` before `SendBrightness(clamped)` at `:144`. Implementation of `ApplyCeilingInPlace` at `:177-184`. | closed |
| T-14.2-04 | Repudiation | Lost ceiling write / out-of-date state | accept | Ceiling lives in controller instance; persistence is VRSettings' responsibility. Debug-only surface. See Accepted Risks log. | closed |
| T-14.2-05 | Information disclosure | Transport error logs | accept | Logs include Win32 error code + literal port name (user-supplied via VRSettings). No secrets. See Accepted Risks log. | closed |
| T-14.2-06 | Elevation of privilege (scope) | `user32` link | accept(scope) | Plan 14-02 only adds `target_link_libraries(... user32)` in `CMakeLists.txt` — no window created, no `RegisterDeviceNotification` in this plan. Hotplug surface introduced in 14-03 with its own mitigation (T-14.3-08). See Accepted Risks log. | closed |
| T-14.2-07 | Spoofing | Transport selection | accept | Transport is hardcoded to `WledSerialTransport` at `device_provider.cpp:1316` via `std::make_unique<WledSerialTransport>()`. No attacker-controlled swap. See Accepted Risks log. | closed |

### Plan 14-03 — Driver Integration

| Threat ID | Category | Component | Disposition | Mitigation | Status |
|-----------|----------|-----------|-------------|------------|--------|
| T-14.3-01 | Tampering | Pipe command parser (`HandleBackglowCommand`) | mitigate | `ParseHex6` at `device_provider.cpp:1278-1292` rejects length != 6 and non-hex digits. `fill` branch rejects invalid hex (`:1380`) and explicitly rejects >10 tokens via post-loop `strtok_s` probe at `:1395-1400`. `set` branch bounds-checks `idx in [0..9]` at `:1439`. `bri` branch rejects `sscanf` fail + `v > 255` at `:1455`. Emits documented `ERR ...` strings on every failure. | closed |
| T-14.3-02 | Tampering | VRSettings string `backglow_com_port` | mitigate | Empty/invalid port -> degraded mode. VRSettings read at `device_provider.cpp:1307-1310`; passed to `LedController::Start` at `:1323`; validated inside `WledSerialTransport::Open` via `IsValidComName` (T-14.2-01). On failure: `m_backglowDisabled = true` at `:1328`, single INFO log gated by `m_backglowDisabledLogged` at `:1329-1334`, driver continues to load. | closed |
| T-14.3-03 | DoS | Pipe handler blocking on serial | mitigate | `HandleBackglowCommand` calls only `QueueFrame` / `QueueLedSet` / `QueueBrightness` / `QueuePower` on `m_pLedController` (lines `:1411, 1417, 1445, 1465, 1473, 1481`). No direct `transport->Send*` call. Queue methods (`led_controller.cpp:199-241`) are bounded: mutex + memcpy + cv signal, O(1). | closed |
| T-14.3-04 | DoS | Hotplug debounce thrash | mitigate | `OnHotplugArrival` at `device_provider.cpp:1566-1580` starts with `Sleep(500)` at `:1570` before re-attempting `Start`. Triggered only on `WM_DEVICECHANGE + DBT_DEVICEARRIVAL + DBT_DEVTYP_DEVICEINTERFACE` filter (`:1264-1268`). | closed |
| T-14.3-05 | Repudiation | Brightness ceiling bypass via pipe | mitigate | No pipe subcommand mutates ceiling. Only `fill`, `set`, `bri`, `off`, `on` implemented; `bri` cannot change `m_ceiling` — it only calls `QueueBrightness(v)` at `device_provider.cpp:1465` and the controller re-clamps at `led_controller.cpp:141-142`. Ceiling source = VRSettings-only (`:1299-1304`). | closed |
| T-14.3-06 | Information disclosure | Pipe response echoes input hex | accept | Echoing user-supplied 6-hex in `OK fill=<hex>` / `OK set idx=N rgb=<hex>` is not a disclosure (caller already knows what they sent). See Accepted Risks log. | closed |
| T-14.3-07 | DoS | `Cleanup()` blocking on hardware | mitigate | `Cleanup` calls `m_pLedController->ShutdownAllOff()` at `device_provider.cpp:142`, which joins the writer and then performs **2 synchronous writes** (`SendRgbFrame(black)` + `SendPower(false)`) at `led_controller.cpp:272-273`. Each write bounded by `WriteAllWithTimeout`'s 100 ms `WaitForSingleObject` + `WriteTotalTimeoutConstant=50`. Max ~200 ms for the backglow Cleanup segment. | closed |
| T-14.3-08 | Elevation of privilege | Message-only window in DLL | mitigate | `HWND_MESSAGE` parent used at `device_provider.cpp:1524` so window is never visible and cannot receive focus. Unique class name `"BeyondBackglowHotplug"` at `:1259`. Destroyed in `HotplugThreadFunc` cleanup path at `:1560-1563` (`DestroyWindow` + `UnregisterClassA`) and invoked from `StopHotplugWatcher` via `PostMessageA(WM_QUIT)` at `:1504`, called from `Cleanup` at `:145`. | closed |

*Status: open · closed*
*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)*

---

## Accepted Risks Log

| Risk ID | Threat Ref | Rationale | Accepted By | Date |
|---------|------------|-----------|-------------|------|
| AR-14-01 | T-14.1-03 | Spike is throwaway CLI run locally. Logs contain COM port name, raw Adalight bytes, and WLED version reply — none are secrets or PII. No persistence beyond console stdout. | brandon@bigscreenvr.com | 2026-04-19 |
| AR-14-02 | T-14.1-04 | Spike binary runs in user space, opens a single COM device with exclusive access, exits. No driver install, no service, no privilege escalation surface. | brandon@bigscreenvr.com | 2026-04-19 |
| AR-14-03 | T-14.2-04 | Ceiling state lives only in the `LedController` instance. Persistence of `backglow_brightness_ceiling` is the VRSettings layer's responsibility (read once at Init). Debug-only control surface — no audit log required at ASVS L1. | brandon@bigscreenvr.com | 2026-04-19 |
| AR-14-04 | T-14.2-05 | Transport error logs at INFO level emit Win32 error code + user-supplied port name. No secrets, no PII. Already in the user's control (they typed the port into VRSettings). | brandon@bigscreenvr.com | 2026-04-19 |
| AR-14-05 | T-14.2-06 | Plan 14-02 links `user32` but introduces no window and no `RegisterDeviceNotification` call. The actual window surface is added in Plan 14-03 under T-14.3-08 which provides its own mitigation (HWND_MESSAGE + destroy-in-cleanup). | brandon@bigscreenvr.com | 2026-04-19 |
| AR-14-06 | T-14.2-07 | `WledSerialTransport` is constructed directly at `device_provider.cpp:1316` via `std::make_unique<WledSerialTransport>()`. No attacker-controllable transport-type field exists in Phase 14. TPM2 stub (`WledTpm2Transport`) is never instantiated. Pluggability returns in Phase 15 with its own threat analysis. | brandon@bigscreenvr.com | 2026-04-19 |
| AR-14-07 | T-14.3-06 | Pipe response echo of user-supplied hex (`OK fill=RRGGBB`) is not a disclosure — caller originated the value. Response format intentionally mirrors input for human debugging. | brandon@bigscreenvr.com | 2026-04-19 |

*Accepted risks do not resurface in future audit runs.*

---

## Unregistered Flags

None. SUMMARY.md `## Threat Flags` sections for all three plans (14-01, 14-02, 14-03) explicitly report "no new threat surface introduced beyond plan's `<threat_model>`." All observed threats in SUMMARY files map to registered IDs above.

---

## Security Audit Trail

| Audit Date | Threats Total | Closed | Open | Run By |
|------------|---------------|--------|------|--------|
| 2026-04-19 | 20 | 20 | 0 | gsd-security-auditor (sonnet) |

---

## Sign-Off

- [x] All threats have a disposition (mitigate / accept / transfer)
- [x] Accepted risks documented in Accepted Risks Log
- [x] `threats_open: 0` confirmed
- [x] `status: verified` set in frontmatter

**Approval:** verified 2026-04-19
