# Bug Report — Security & Correctness Audit

**Date:** 2026-06-26
**Scope:** `Source/` (C++ MARTe2 components + StreamHub app) and `Client/` (Go web clients, C++ ImGui/Qt desktop clients, JS web SPAs)
**Method:** Static source review of all network-facing, binary-parsing, concurrency, and web-serving code.

---

## Severity scale

| Level | Meaning |
|-------|---------|
| **Critical** | Remote code execution, drive-by takeover, or heap corruption reachable from the network. |
| **High** | Remote crash (OOM/panic/abort), out-of-bounds read/write, use-after-free, unauthenticated control of the real-time plant. |
| **Medium** | Data corruption, denial-of-service, parser confusion, RFC violations, races with bounded impact. |
| **Low** | Latent/fragile code, documentation mismatches, non-exploitable UB, missing hardening. |

---

## CRITICAL

### CR-1 — 1-byte heap OOB write in WebSocket frame NUL-termination

| Field | Value |
|-------|-------|
| File | `Source/Applications/StreamHub/WSServer.cpp:251, 315-316` |
| Severity | Critical |
| Type | Heap buffer overflow |
| Attack vector | Remote TCP (any client that completes the WS handshake) |

**Vulnerable code:**
```cpp
static const uint32 kRecvBuf = WS_MAX_RECV_PAYLOAD + 14u;  // 65536 + 14 = 65550
uint8 *buf = new uint8[kRecvBuf];        // valid indices 0..65549
...
// payload = frameStart + hdr.headerSize  (headerSize can be 14)
// plen   = up to WS_MAX_RECV_PAYLOAD (65536)
uint8 savedByte = payload[plen];         // payload[65536] == buf[65550] — OOB read
payload[plen] = '\0';                    // OOB write one byte past the heap allocation
```

**Root cause:** The comment `/* safe: buf has extra byte */` is incorrect. The buffer is exactly `65536 + 14 = 65550` bytes. A masked WebSocket frame with 64-bit extended length (`headerSize = 14`) and `payloadLen = 65536` fills the entire buffer; `payload[plen]` then writes one byte past the end.

**Impact:** Heap corruption — potential code execution depending on allocator/heap layout.

**Fix:** Allocate one extra byte:
```cpp
static const uint32 kRecvBuf = WS_MAX_RECV_PAYLOAD + 14u + 1u;
```

---

### CR-2 — XSS via unescaped `src.addr` in stats panel

| Field | Value |
|-------|-------|
| Files | `Client/udpstreamer/static/app.js:3503`; `Client/debugger/static/app.js:3549` |
| Severity | Critical |
| Type | Cross-site scripting (stored/reflected via server data) |
| Attack vector | Malicious or compromised StreamHub/DebugService feeding crafted source address |

**Vulnerable code:**
```js
body.innerHTML = `... ${_statsKV('Address', src.addr)} ...`;

function _statsKV(label, value, cls) {
  return `<div class="stats-kv"><span class="stats-k">${label}</span>` +
         `<span class="stats-v${cls ? ' ' + cls : ''}">${value}</span></div>`;
}
```

`_statsKV` interpolates `value` raw into HTML. `src.addr` originates from the WebSocket server's `sources`/`addSource` JSON. A crafted address such as `<img src=x onerror=alert(1)>` achieves arbitrary JavaScript execution in the browser, which can then drive the oscilloscope and trigger over the same WS connection.

**Impact:** Browser-side RCE → full control of the oscilloscope/trigger/MARTe2 debug interface via the compromised WS session.

**Fix:** Escape `src.addr` with the existing `escHtml()` helper before interpolation, or use `textContent`/`createElement`.

---

### CR-3 — WebSocket CSRF: `CheckOrigin` always returns `true` (Go) / no Origin check (C++)

| Field | Value |
|-------|-------|
| Files | `Common/Client/go/wshub/hub.go:128` (shared by `Client/udpstreamer` + `Client/debugger`); `Source/Applications/StreamHub/WSServer.cpp:186-239` |
| Severity | Critical |
| Type | Cross-Site WebSocket Hijacking (CSWSH / CSRF) |
| Attack vector | Drive-by web page |

**Vulnerable code (Go):**
```go
var upgrader = websocket.Upgrader{
    ReadBufferSize:  4096,
    WriteBufferSize: 64 * 1024,
    CheckOrigin:     func(r *http.Request) bool { return true },
}
```

**Vulnerable code (C++):** `UpgradeHTTP` parses `Sec-WebSocket-Key` and computes the accept hash but never reads or validates the `Origin` header.

**Impact:** Any malicious web page visited by the user can open a WebSocket to `ws://localhost:8080/ws` (or `:9090`, or the C++ hub's `:8090`) and send JSON commands — `addSource`, `arm`, `setTrigger`, `removeSource`, and in the debugger `PAUSE`/`RESUME`/`MSG`/`TRACE`/`FORCE`/`BREAK` against the live MARTe2 instance. Combined with CR-4 this is a full drive-by takeover of the plant controller.

**Fix:** Validate `Origin` against a configurable allowlist (same-host at minimum); reject cross-origin upgrade requests.

---

### CR-4 — Unauthenticated arbitrary command injection to MARTe2

| Field | Value |
|-------|-------|
| File | `Client/debugger/martecontrol.go:217-263` |
| Severity | Critical |
| Type | Command injection / unauthorized control |
| Attack vector | Drive-by web page (via CR-3) or direct TCP |

**Vulnerable code:**
```go
case "cmd":
    data, _ := env["data"].(map[string]interface{})
    if data == nil { return }
    cmd, _ := data["cmd"].(string)
    if cmd != "" {
        m.trackForcedCmd(cmd)
        m.SendCommand(cmd)   // raw string sent to MARTe2 TCP control
    }
```

**Impact:** Any web page (via CR-3) or any TCP client can send `FORCE`, `TRACE`, `PAUSE`, `RESUME`, `STEP`, `MSG`, `BREAK` to the real-time control system.

**Fix:** Require authentication (token-based) for command-sending WS messages; validate `cmd` against an allowlist of known MARTe2 commands; fix the Origin check (CR-3).

---

### CR-5 — No authentication on DebugService TCP command interface

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/DebugService/DebugService.cpp:276` |
| Severity | Critical |
| Type | Missing authentication / unauthorized control |
| Attack vector | Direct TCP to port 8080 |

**Vulnerable code:**
```cpp
BasicTCPSocket *newClient = tcpServer.WaitConnection(TimeoutType(100));
```

Any TCP client that connects can send `FORCE` (overwrite signal memory in the RT app), `PAUSE` (halt the RT loop), `STEP`, `TRACE`, and `MSG` (invoke any message handler on any ORD object — see HI-9). There is no authentication, TLS, or access control.

**Impact:** Remote control of the real-time plant: forced signal values, paused execution, arbitrary message dispatch.

**Fix:** Bind to localhost by default; add a shared-secret token or TLS gate; restrict `MSG` to whitelisted destinations.

---

## HIGH

### HI-1 — Integer overflow in DATA bounds check → heap OOB read

| Field | Value |
|-------|-------|
| Files | `Source/Applications/StreamHub/UDPSourceSession.cpp:358`; `Source/Components/DataSources/UDPStreamerClient/UDPStreamerClient.cpp:520` (same bug mirrored) |
| Severity | High |
| Type | Integer overflow → out-of-bounds read |
| Attack vector | Single crafted UDP DATA packet |

**Vulnerable code (StreamHub):**
```cpp
// numSamples read directly from attacker-controlled UDP payload (line 333-338)
if (pm == UDPS_PUBLISH_ACCUMULATE) {
    memcpy(&numSamples, payload + offset, 4u);
    if (numSamples == 0u) { numSamples = 1u; }
}
uint32 elemsToRead = (pm==ACCUMULATE && numElements==1) ? numSamples : numElements;
if (off + elemsToRead * wireElemBytes > size) { return; }  // 32-bit multiply wraps
```

`elemsToRead` is fully attacker-controlled via `numSamples`. `elemsToRead * wireElemBytes` is a `uint32` multiplication that can wrap to a small value (e.g. `0x20000001 * 8 = 0x8`), causing the bounds check to pass. The subsequent `DecodeElems` loop then reads far past the payload buffer.

**Impact:** Heap OOB read → crash or information leak.

**Fix:** Use 64-bit arithmetic for the bounds check:
```cpp
uint64 bytesNeeded = static_cast<uint64>(off)
    + static_cast<uint64>(elemsToRead) * static_cast<uint64>(wireElemBytes);
if (bytesNeeded > static_cast<uint64>(size)) { return; }
```

---

### HI-2 — Unbounded allocations from a single crafted UDP datagram (Go decoder)

| Field | Value |
|-------|-------|
| File | `Common/Client/go/udpsprotocol/protocol.go:121, 229, 325` |
| Severity | High |
| Type | Denial of service (OOM/panic) |
| Attack vector | Single crafted UDP CONFIG or DATA packet |

**Vulnerable code:**
```go
// protocol.go:121 — NumElements()
func (s SignalInfo) NumElements() int {
    r := int(s.NumRows); c := int(s.NumCols)
    if r == 0 { r = 1 }; if c == 0 { c = 1 }
    return r * c   // no overflow check; (2^32-1)^2 overflows int64 → negative → panic
}

// protocol.go:229 — ParseConfig()
numSigs := binary.LittleEndian.Uint32(payload[0:4])
sigs := make([]SignalInfo, 0, numSigs)   // numSigs=0xFFFFFFFF → ~480 GB alloc → OOM

// protocol.go:325 — ParseData()
numSamples := int(binary.LittleEndian.Uint32(payload[8:12]))
samples := make([]DataSample, numSamples)   // numSamples=0xFFFFFFFF → OOM
```

**Impact:** A single 17-byte UDP datagram crashes the Go web UI / chain client (panic or OOM kill).

**Fix:** Validate `numSigs`/`numSamples`/`NumRows*NumCols` against `len(payload)/elementSize` before allocating. Cap `NumElements()` at a sane maximum (e.g. 1M).

---

### HI-3 — `accumFill` increment before bounds check → buffer overflow

| Field | Value |
|-------|-------|
| File | `Source/Components/DataSources/UDPStreamer/UDPStreamer.cpp:857-860` |
| Severity | High |
| Type | Buffer overflow |
| Attack vector | Configuration-driven (signal/payload size mismatch) |

**Vulnerable code:**
```cpp
uint8 *slot = accumBuffer + (accumFill * totalSrcBytes);
(void) MemoryOperationsHelper::Copy(slot, memory, totalSrcBytes);
accumTimestamps[accumFill] = ts;
accumFill++;                                  // incremented unconditionally
// flush check at 866-871 only fires if payload-size/time conditions hit;
// if they don't and accumFill == maxBatchCount, the next call writes past the buffer
```

Related: the `maxBatchCount * totalSrcBytes` buffer-size calculation at line 700 (and 738, 757) also overflows `uint32`, allocating a too-small buffer.

**Impact:** Heap buffer overflow in the Accumulate publishing path.

**Fix:** Check `accumFill >= maxBatchCount` before writing; force-flush if so. Use `uint64` for the size calculation.

---

### HI-4 — `ProcessSignal` memcpy uses unclamped broker `size` → OOB read from `forcedValue[1024]`

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/DebugService/DebugServiceBase.cpp:310, 313-318` |
| Severity | High |
| Type | Out-of-bounds read |
| Attack vector | Forcing a signal whose runtime byte size exceeds the `ForceSignal`-validated size |

**Vulnerable code:**
```cpp
if (nEl <= 1u) {
    memcpy(signalInfo->memoryAddress, signalInfo->forcedValue, size);
    // size comes from broker GetCopyByteSize, not from ForceSignal validation (line 420)
```

`ForceSignal` rejects signals > 1024 bytes (line 420), but the runtime `size` comes from `GetCopyByteSize(j)` and can exceed 1024, reading past `forcedValue[1024]`. Additionally, the array-forcing loop at line 313-318 reads `forcedMask[e >> 3]` for `e` up to `nEl`; `forcedMask` is only 32 bytes (256 bits), so `nEl > 256` causes an OOB read of `forcedMask`. `ForceSignal` at line 445-448 sets bits up to `numberOfElements`, which can exceed 256, also writing past `forcedMask`.

**Impact:** OOB heap read; potential OOB write to `forcedMask`.

**Fix:** Clamp `size` to `sizeof(signalInfo->forcedValue)` in `ProcessSignal`; validate `nEl <= 256` in `RegisterSignal`/`ForceSignal`; cap the array-forcing loop at `min(nEl, 256)`.

---

### HI-5 — Race / use-after-free: BroadcastText vs FreeSlot

| Field | Value |
|-------|-------|
| File | `Source/Applications/StreamHub/WSServer.cpp:345-366` (Broadcast) vs `432-445` (FreeSlot) |
| Severity | High |
| Type | Use-after-free / data race |
| Attack vector | Triggered by client disconnect during broadcast |

**Vulnerable code (BroadcastText):**
```cpp
void WSServer::BroadcastText(const char *json, uint32 len) {
    for (uint32 i = 0u; i < WS_MAX_CLIENTS; i++) {
        if (!clients[i].active) { continue; }          // no lock held
        (void) clients[i].writeMutex.FastLock();
        if (clients[i].active) {                        // re-check under writeMutex
            (void) SendFrame(clients[i], ...);          // uses clients[i].sock
        }
        clients[i].writeMutex.FastUnLock();
    }
}
```

**Vulnerable code (FreeSlot):**
```cpp
void WSServer::FreeSlot(uint32 idx) {
    (void) clientsMutex.FastLock();           // holds clientsMutex, NOT writeMutex
    if (clients[idx].active) {
        clients[idx].active = false;
        if (clients[idx].sock) {
            clients[idx].sock->Close();
            delete clients[idx].sock;          // frees the socket
            clients[idx].sock = NULL;
        }
    }
    clientsMutex.FastUnLock();
}
```

`FreeSlot` (read loop, on disconnect) holds `clientsMutex` and `delete`s `sock`. `BroadcastText`/`BroadcastBinary` (push thread) holds only `writeMutex` and dereferences `sock`. There is no mutual exclusion on `active`/`sock` between these two code paths.

**Impact:** Use-after-free → crash or potential code execution.

**Fix:** `FreeSlot` must acquire `writeMutex` before modifying `active`/`sock`, or `BroadcastText`/`BroadcastBinary` must hold `clientsMutex` during the iteration.

---

### HI-6 — `FD_SET` with `fd >= FD_SETSIZE` → stack buffer overflow

| Field | Value |
|-------|-------|
| Files | `Source/Components/Interfaces/UDPStream/UDPSServer.cpp:273, 308`; `Source/Components/Interfaces/UDPStream/UDPSClient.cpp:383` |
| Severity | High |
| Type | Stack buffer overflow |
| Attack vector | Many open file descriptors (many TCP clients, high ulimit) |

**Vulnerable code:**
```cpp
int fd = tcpClients[i]->GetReadHandle();
fd_set rset;
FD_ZERO(&rset);
FD_SET(fd, &rset);               // if fd >= FD_SETSIZE (typically 1024), stack buffer overflow
```

`FD_SET` writes to a fixed-size stack bitmap (`fd_set` is typically 1024 bits / 128 bytes). If `fd >= FD_SETSIZE`, it writes past the bitmap. The multicast listener path already uses `poll()` but existing-client polling reverts to `select()`.

**Impact:** Stack buffer overflow when the process has many open FDs.

**Fix:** Check `fd < FD_SETSIZE` before calling `FD_SET`, or switch to `poll()`/`epoll` (which the codebase already uses elsewhere).

---

### HI-7 — Weak PRNG for WebSocket handshake key

| Field | Value |
|-------|-------|
| File | `Client/streamhub/WSClient.cpp:29-31` |
| Severity | High |
| Type | Weak randomness |
| Attack vector | Predict key → MITM handshake |

**Vulnerable code:**
```cpp
static std::string base64Key() {
    uint8_t raw[16];
    srand(static_cast<unsigned>(time(nullptr)));       // second-granularity, global RNG reseed
    for (int i = 0; i < 16; i++) {
        raw[i] = static_cast<uint8_t>(rand() & 0xFF);  // rand() often only 15-32 bits entropy
    }
    ...
}
```

`srand(time(nullptr))` is predictable to second granularity. `rand()` is not cryptographically secure. An attacker who knows the approximate connection time can predict the `Sec-WebSocket-Key`. Calling `srand` on every `base64Key()` invocation also reseeds the global C RNG, affecting any other `rand()` consumer.

**Impact:** Predictable WS handshake key; potential MITM. Global RNG pollution.

**Fix:** Use a CSPRNG (`getrandom()`, `/dev/urandom`, or `std::random_device`).

---

### HI-8 — Global registry patching of all brokers

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/DebugService/DebugServiceBase.cpp:217-242` |
| Severity | High |
| Type | Design-level global state mutation |
| Attack vector | N/A (correctness/design) |

**Vulnerable code:**
```cpp
void DebugServiceBase::PatchRegistry() {
    PatchItemInternal("MemoryMapInputBroker",      new DebugMemoryMapInputBrokerBuilder());
    PatchItemInternal("MemoryMapInputOutputBroker", new DebugMemoryMapInputOutputBrokerBuilder());
    // ... 10 more broker types
}

static void PatchItemInternal(const char8 *originalName, ObjectBuilder *debugBuilder) {
    ClassRegistryItem *item = ClassRegistryDatabase::Instance()->Find(originalName);
    if (item != NULL_PTR(ClassRegistryItem *)) {
        item->SetObjectBuilder(debugBuilder);   // globally replaces the builder
    }
}
```

This globally replaces the `ObjectBuilder` for all standard broker classes process-wide, silently, on `Initialise()`. Implications:
- The `new Debug*BrokerBuilder()` allocations are never freed (memory leak, intentional for process lifetime).
- Original builders are not saved/restored — if `DebugService` is destroyed, patched builders remain.
- A second `DebugService` instance double-patches; the first's builders are leaked.
- Any `dynamic_cast` to the original broker type breaks (gets a debug wrapper instead).

**Fix:** Document prominently; make patching opt-in rather than automatic on `Initialise()`; save/restore original builders.

---

### HI-9 — `TraceRingBuffer` not thread-safe

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/DebugService/DebugCore.h:79-142` |
| Severity | High |
| Type | Data race |
| Attack vector | N/A (correctness) |

**Vulnerable code:**
```cpp
uint32 Push(const uint8 *data, uint32 size) {   // called from RT broker thread under tracePushMutex
    ...
    uint32 w = writeIndex;                       // volatile uint32 — no memory barrier
    ...
    writeIndex = next;                           // volatile write
}

uint32 Pop(uint8 *dst, uint32 maxBytes) {        // called from Streamer thread, NO LOCK
    uint32 r = readIndex;                        // volatile read
    uint32 w = writeIndex;                       // volatile read — can be torn/stale
    ...
}
```

`Push` (RT broker thread, under `tracePushMutex`) and `Pop` (streamer thread, **no lock**) communicate via `volatile uint32` indices. `volatile` does not provide atomicity or memory ordering on most architectures (it only prevents compiler reordering, not CPU reordering). A torn/stale read of `writeIndex` can cause `Pop` to read partially-written data or miss entries.

**Impact:** Trace data corruption / lost trace samples / potential torn reads.

**Fix:** Use `Atomic::Load`/`Atomic::Store` for `readIndex`/`writeIndex`, or acquire `tracePushMutex` in `Pop` as well (adds RT path latency — prefer a proper lock-free SPSC ring).

---

## MEDIUM

### MD-1 — Fragment reassembly `recvMask` too small for `totalFragments` up to 512

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/UDPStream/UDPSClient.cpp:544, 592-594, 630-636` |
| Severity | Medium |
| Type | Logic bug / data corruption |

`totalFragments` is capped at 512 (line 544), but `recvMask` is only 32 bytes (256 bits). For `fragIdx >= 256`, the duplicate check at line 594 is skipped (`byteIdx >= 32`). An attacker can send the same high-index fragment repeatedly; each is accepted (overwriting the same data), `receivedFragments` is incremented each time, and `DeliverAssembled` is triggered prematurely with an incomplete reassembly — delivering partially-filled payload data to the application.

**Fix:** Cap `totalFragments` at 256, or enlarge `recvMask` to 64 bytes (512 bits).

---

### MD-2 — Fragment reassembly: no type matching → CONFIG/DATA type confusion

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/UDPStream/UDPSClient.cpp:548-555` |
| Severity | Medium |
| Type | Logic bug / data corruption |

Reassembly slots are keyed only on `counter`, not `type`. The protocol does not guarantee disjoint counter spaces for DATA and CONFIG. An attacker (or buggy server) can send DATA and CONFIG fragments with the same counter; they mix into the same reassembly buffer. The `type` field from the first-arriving fragment determines delivery destination, but the payload is a mixture.

**Fix:** Include `type` in the slot lookup: `reassemblySlots[i].counter == counter && reassemblySlots[i].type == hdr->type`.

---

### MD-3 — Signal name/unit not null-terminated after `memcpy` → intra-struct OOB read

| Field | Value |
|-------|-------|
| File | `Source/Applications/StreamHub/UDPSourceSession.cpp:219-223` |
| Severity | Medium |
| Type | Out-of-bounds read (intra-struct) |

After `memcpy(&sigDescs_[i], payload + ..., UDPS_SIGNAL_DESC_SIZE)`, `name[64]` and `unit[32]` are not force-null-terminated. If the CONFIG payload fills all 64 name bytes without `\0`, subsequent `strcmp(descs[s].name, ...)` (line 968) and `snprintf(key, ..., descs[i].name)` (line 916) read past `name` into `typeCode`, `quantType`, etc. until a zero byte is found.

**Fix:** After the memcpy:
```cpp
sigDescs_[i].name[UDPS_MAX_SIGNAL_NAME - 1u] = '\0';
sigDescs_[i].unit[UDPS_MAX_UNIT_LEN - 1u] = '\0';
```

---

### MD-4 — `numRows * numCols` integer overflow in CONFIG/DATA parsing

| Field | Value |
|-------|-------|
| Files | `Source/Applications/StreamHub/UDPSourceSession.cpp:240, 346`; `Common/Client/go/udpsprotocol/protocol.go:121` |
| Severity | Medium |
| Type | Integer overflow → data corruption |

`numRows` and `numCols` are `uint32` from the CONFIG payload. Their product can overflow `uint32` (e.g. `0x10000 * 0x10000 = 0`), producing a too-small scratch buffer and wrong `numElements` for DATA decoding.

**Fix:** Use 64-bit multiplication and cap against a sane maximum.

---

### MD-5 — SHA1 stack buffer overflow for inputs > 119 bytes (latent)

| Field | Value |
|-------|-------|
| Files | `Source/Applications/StreamHub/SHA1.h:50`; `Client/streamhub/WSFrame_client.h:113` |
| Severity | Medium |
| Type | Stack buffer overflow (latent) |

```cpp
uint8 msg[128];
memcpy(msg, data, len);   // if len > 120, overflows msg[128]
msg[len] = 0x80u;         // OOB if len >= 128
```

Currently only called with the WS key+GUID (~60 bytes), so not exploitable today. `WSFrame_client.h:113` also has `uint32_t bitLen = len * 8u` which truncates for `len > 512MB`, and `new uint8_t[msgLen]()` without exception guard (leaks on `bad_alloc`).

**Fix:** Add `if (len > 119u) return;` guard; use `uint64_t bitLen`; use `std::vector` instead of manual `new[]`/`delete[]`.

---

### MD-6 — No authentication on UDP CONNECT/DISCONNECT/ACK

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/UDPStream/UDPSServer.cpp:655-723, 729-741` |
| Severity | Medium |
| Type | Missing authentication |

`HandleUnicastConnect` accepts any UDP datagram with the right magic and `type=CONNECT` from any source address. `HandleUnicastDisconnect` evicts any client by source address. `HandleUnicastAck` refreshes any client's `lastSeenTicks`. Any host that can send UDP to the server port can: (a) register as a client and receive all streamed data, (b) disconnect any known client by spoofing their source address (DoS), (c) keep a spoofed client alive indefinitely.

**Fix:** Document the trust boundary; if deployed beyond a trusted LAN, add a shared-secret token to the CONNECT payload and validate source addresses.

---

### MD-7 — `StringHelper::Copy` potential buffer overflow in TcpLogger

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/TCPLogger/TcpLogger.cpp:87` |
| Severity | Medium |
| Type | Potential buffer overflow |

```cpp
StringHelper::Copy(entry.description, description);
```

`entry.description` is `char8[MAX_ERROR_MESSAGE_SIZE]`. If `description` (from `logPage->errorStrBuffer`) is longer than `MAX_ERROR_MESSAGE_SIZE - 1` and `StringHelper::Copy` uses `strcpy` internally, this overflows.

**Fix:** Use `strncpy` with explicit size, or verify `StringHelper::Copy` is bounds-safe.

---

### MD-8 — TcpLogger SPSC queue uses `volatile` indices, no atomics; lost wakeup

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/TCPLogger/TcpLogger.cpp:83-153, 157-158` |
| Severity | Medium |
| Type | Data race / lost wakeup |

`writeIdx`/`readIdx` are `volatile uint32` — no memory ordering on non-x86. `eventSem.Wait(10)` followed by `eventSem.Reset()` is not atomic; a post between `Wait` returning and `Reset` is lost (entry delayed up to 10ms).

**Fix:** Use `Atomic::Load`/`Store`; use `ResetWait` for atomic reset+wait.

---

### MD-9 — `printf`/`fflush` on LoggerService (possibly RT) thread

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/TCPLogger/TcpLogger.cpp:75-76` |
| Severity | Medium |
| Type | RT latency jitter |

`ConsumeLogMessage` is called by `LoggerService`, which may run on an RT thread. `printf`/`fflush` are blocking I/O calls causing latency jitter.

**Fix:** Make stdout mirroring optional via configuration.

---

### MD-10 — No cap on WebSocket client connections (Go hub)

| Field | Value |
|-------|-------|
| File | `Common/Client/go/wshub/hub.go:367-377` |
| Severity | Medium |
| Type | Resource exhaustion |

`HandleWebSocket` unconditionally creates a new `wsClient` with a 64-message buffered channel. No cap on total clients. An attacker can open thousands of connections, exhausting memory and goroutines.

**Fix:** Track `len(h.clients)`; reject new connections above a configurable maximum (e.g. 100).

---

### MD-11 — Silent data loss on all non-blocking channel sends

| Field | Value |
|-------|-------|
| File | `Common/Client/go/wshub/hub.go:346-358, 315-318, 322-326, 330-334, 340-342` |
| Severity | Medium |
| Type | Silent data loss |

`PushDataForSource`, `broadcast`, `AddSource`, `RemoveSource`, `SetSourceState`, `UpdateConfigForSource` all use `select { case ch <- v: default: }`, silently dropping messages when channels are full. Under high data rates, `dataCh` (cap 65536) fills and samples are dropped with no metric or backpressure.

**Fix:** Add a dropped-counter metric; log/alert when drops occur; consider backpressure.

---

### MD-12 — SSRF via unauthenticated `addSource`

| Field | Value |
|-------|-------|
| Files | `Common/Client/go/wshub/hub.go:83-96`; `Common/Client/go/wshub/sources.go:62-67` |
| Severity | Medium |
| Type | Server-Side Request Forgery |

Any WebSocket client (any origin due to CR-3) can add a data source pointing to any `host:port`. The `addr` is passed to `net.ResolveUDPAddr`/`net.ResolveTCPAddr` and the client dials it. This is SSRF — an attacker can probe internal network services.

**Fix:** Validate/allow-list addresses; require authentication for source management.

---

### MD-13 — Reassembler unbounded fragment-set map growth (Go)

| Field | Value |
|-------|-------|
| File | `Common/Client/go/udpsprotocol/reassembler.go:41-89` |
| Severity | Medium |
| Type | Memory exhaustion |

An attacker sending UDP packets with unique `(counter, type)` keys and `TotalFragments=2, FragmentIdx=0` creates a new `fragmentSet` per packet. Each set holds payload bytes and a `make([][]byte, total)` (up to 65535 entries). Within the 2-second TTL, high-rate flooding exhausts memory.

**Fix:** Cap the number of concurrent fragment sets (e.g. `maxSets = 1024`); reject new sets when the cap is reached.

---

### MD-14 — `martecontrol.go` index panic on short "OK SERVICE_INFO" response

| Field | Value |
|-------|-------|
| File | `Client/debugger/martecontrol.go:543` |
| Severity | Medium |
| Type | Panic / crash |

```go
if strings.HasPrefix(line, "OK SERVICE_INFO") {   // matches 15-char string
    ...
    "data": line[len("OK SERVICE_INFO "):],       // line[16:] → panic if len(line)==15
```

**Fix:** Use `strings.TrimPrefix` and handle the empty case, or add a length check.

---

### MD-15 — `pairCount * 16u` overflow on 32-bit (shared C++ client wire layer)

| Field | Value |
|-------|-------|
| File | `Client/streamhub/Protocol.cpp:77, 117` (reused by Qt client) |
| Severity | Medium (32-bit), Low (64-bit) |
| Type | Integer overflow → OOB |

```cpp
uint32_t pairCount = readU32(buf, off, len);
if (off + static_cast<size_t>(pairCount) * 16u > len) { return false; }
```

On 32-bit, `pairCount * 16u` can overflow, bypassing the bounds check. On 64-bit, `pairCount` can be up to ~268M, forcing a large allocation.

**Fix:** Check `pairCount > (len - off) / 16` before multiplication; use `ull` suffix.

---

### MD-16 — `readU16`/`readU32` return 0 on truncation without signaling

| Field | Value |
|-------|-------|
| File | `Client/streamhub/Protocol.cpp:21-38, 66-76` |
| Severity | Medium |
| Type | Silent malformed-frame acceptance |

```cpp
static uint16_t readU16(const uint8_t* buf, size_t& off, size_t len) {
    if (off + 2 > len) { return 0u; }   // returns 0, does NOT signal failure
    ...
}
```

If the buffer is truncated at the `numSignals` field, `readU32` returns 0 and `off` is not advanced. `ParseBinaryFrame` then returns `true` with an empty signal list, silently accepting a truncated/malformed frame as valid.

**Fix:** `readU16`/`readU32`/`readF64` should return a status or set a flag; `ParseBinaryFrame` should fail fast on any truncated read.

---

### MD-17 — JSON command builders use `snprintf`+`%s` with no escaping → JSON injection

| Field | Value |
|-------|-------|
| File | `Client/streamhub/Protocol.cpp:183-186, 209-213` |
| Severity | Medium |
| Type | JSON injection |

```cpp
char buf[256];
snprintf(buf, sizeof(buf), "{\"type\":\"getConfig\",\"sourceId\":\"%s\"}",
         sourceId.c_str());
```

`%s` with user/server-provided strings does not escape JSON special characters. A source label containing `"` breaks the JSON and can inject arbitrary JSON keys. Also `snprintf` truncates silently for long inputs, producing invalid JSON.

**Fix:** Use a JSON builder that escapes strings; use `std::string` instead of fixed buffers.

---

### MD-18 — `strstr`-based JSON parsing matches nested keys

| Field | Value |
|-------|-------|
| File | `Client/streamhub/Protocol.cpp:296-310, 495, 510` |
| Severity | Medium |
| Type | Parser confusion / data corruption |

`ParseSources` uses `strstr(p, "\"id\":\"")` which matches inside string values. `ParseZoom`/`ParseStats` use `strstr(p, "\"t\":[")` which matches inside a signal key name. A crafted source label containing `"id":"` injects phantom sources.

**Fix:** Migrate to a real JSON parser (nlohmann/json for ImGui; QJsonDocument for Qt).

---

### MD-19 — WebSocket RFC 6455 violations in ImGui client

| Field | Value |
|-------|-------|
| File | `Client/streamhub/WSClient.cpp:204-223` |
| Severity | Medium |
| Type | RFC non-compliance |

- No `CONTINUATION` opcode handling (opcode 0x00) — fragmented messages silently dropped.
- No ≤125 byte enforcement on control frames — a malicious server can send a 16MB ping, echoed as a 16MB pong (bandwidth amplification).
- `CLOSE` frame not echoed back (RFC §5.5.1 requires it); close status code not read.

**Fix:** Implement continuation-frame reassembly; reject control frames with `payloadLen > 125`; echo close frame.

---

### MD-20 — Handshake `recv` one byte at a time, no `SO_RCVTIMEO`

| Field | Value |
|-------|-------|
| File | `Client/streamhub/WSClient.cpp:290-301` |
| Severity | Medium |
| Type | Denial of service |

If the server sends a partial response and hangs, the receive thread blocks forever (the 3-second reconnect sleep never runs).

**Fix:** Set `SO_RCVTIMEO` on the socket; read in larger chunks.

---

### MD-21 — 64KB stack buffer in DebugService streamer thread + shadowed member

| Field | Value |
|-------|-------|
| File | `Source/Components/Interfaces/DebugService/DebugService.cpp:438, 489` |
| Severity | Medium |
| Type | Stack pressure |

```cpp
uint8 udpsSampleBuf[UDPS_MAX_SAMPLE_BYTES];   // local 65510 bytes on stack (line 438)
...
static const uint32 CFG_BUF_SIZE = 65535u;
uint8 cfgBuf[CFG_BUF_SIZE];                    // another 64KB on stack (line 489)
```

The class also has a member `uint8 udpsSampleBuf[65535u]` (DebugService.h:156) that is shadowed by the local and never used.

**Fix:** Use the member instead of the local; use heap allocation for `cfgBuf`.

---

### MD-22 — `FastPollingMutexSem` (spinlock) on RT path with network I/O in background holder

| Field | Value |
|-------|-------|
| File | `Source/Components/DataSources/UDPStreamer/UDPStreamer.cpp:856, 947-976` |
| Severity | Medium |
| Type | Priority inversion / unbounded latency |

`Synchronise()` (RT thread) uses `bufMutex.FastLock(TTInfiniteWait)`. The background thread holds `bufMutex` during a `memcpy` of `fill * totalSrcBytes` (potentially large) at line 950-955. If the background thread is preempted while holding the spinlock, the RT thread spins indefinitely, missing its deadline.

**Fix:** Use a non-spinning mutex for the background thread, or ensure the RT-side critical section is minimal (pointer swap, not full memcpy). Consider a triple-buffer.

---

### MD-23 — `configValidated` read without lock

| Field | Value |
|-------|-------|
| File | `Source/Components/DataSources/UDPStreamerClient/UDPStreamerClient.cpp:463` |
| Severity | Medium |
| Type | Data race |

`configValidated` is set under `bufMutex` (line 453) and cleared under `bufMutex` (line 489), but read without the lock at line 463. Technically UB (benign for a `bool` on x86).

**Fix:** Mark `volatile` or acquire the lock before reading.

---

### MD-24 — `parseCapture` panics on truncated input (E2E chain client)

| Field | Value |
|-------|-------|
| File | `Test/E2E/chain/client/main.go:140-171` |
| Severity | Medium |
| Type | Panic / crash |

`parseCapture` reads `keyLen`, `key`, and `n` without checking `off` is within `len(b)`. A truncated v2 frame causes a runtime panic (slice bounds out of range). The only initial check is `len(b) < 29`.

**Fix:** Add bounds checks before each read (`if len(b) < off+2 { return nil, error }`), mirroring `parsePush`.

---

## LOW

### LO-1 — `totalFrags` overflow (caller-controlled)

| File | `Source/Components/Interfaces/UDPStream/UDPSServer.cpp:541-542` |
| Severity | Low |

`payloadSize + maxChunk - 1u` overflows if `payloadSize` is near `UINT32_MAX`. Not network-controlled but a caller bug could trigger it.

---

### LO-2 — `Stop()` TOCTOU on socket deletion

| File | `Source/Applications/StreamHub/WSServer.cpp:104-134` |
| Severity | Low |

`Stop()` calls `sock->Close()` then `Sleep(200ms)` then `delete sock`. The read thread holds a raw `sock` copy. The 200ms sleep may not be sufficient; `delete` could free the socket while the read thread still references it.

**Fix:** Use thread join instead of fixed sleep.

---

### LO-3 — `FastPollingMutexSem` priority inversion on contended non-RT paths

| Files | `UDPSourceSession.h:302, 311, 370`; `WSServer.h:60, 122` |
| Severity | Low |

Spinlocks used for `metaMutex_`, `statsMutex_`, `recSpecMutex_`, `clientsMutex`, `writeMutex`. Priority inversion with no bound if a low-priority holder is preempted by a high-priority spinner.

---

### LO-4 — `SignalBuffer::push` with `capacity == 0` → mod-0 UB

| File | `Client/streamhub/SignalBuffer.h:36-41` |
| Severity | Low |

`head = (head + 1) % capacity` is UB if `capacity == 0`. Not currently reachable (default cap 20000; `onMaxPointsUpdated` guards `mp >= 2`), but the API is fragile.

---

### LO-5 — `SignalBuffer` comment claims "Thread-safe" but has no internal locking

| File | `Client/streamhub/SignalBuffer.h:18` |
| Severity | Low |

Safe only by current call-site discipline (ImGui: main-thread access; Qt: GUI-thread signals). Misleading comment could cause a future refactor to introduce a data race.

---

### LO-6 — SineArrayGAM / TimeArrayGAM: no output-type validation

| Files | `Source/Components/GAMs/SineArrayGAM/SineArrayGAM.cpp:81-100`; `Source/Components/GAMs/TimeArrayGAM/TimeArrayGAM.cpp:54-81` |
| Severity | Low |

`Setup()` never verifies the output signal type. `TimeArrayGAM.h` doc says `uint32` but code uses `uint64` → if user follows the doc and configures `uint32`, `nElements` is doubled and writes overflow the buffer.

**Fix:** Add `GetSignalType(...)` checks; update `TimeArrayGAM.h` doc to `uint64`.

---

### LO-7 — Signal names inserted into JSON via `%s` Printf without escaping

| File | `Source/Components/Interfaces/DebugService/DebugServiceBase.cpp:900-906` |
| Severity | Low |

Config-sourced (not remote), but a format violation. A signal name containing `"` or `\` breaks the JSON.

**Fix:** Use the existing `EscapeJson` helper.

---

### LO-8 — `EvaluateBreak` checks only element 0 of array signals

| File | `Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h:61-86` |
| Severity | Low |

Undocumented feature limitation.

---

### LO-9 — `fprintf(stderr, ...)` on init path

| File | `Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h:195-197` |
| Severity | Low |

Should use `REPORT_ERROR` instead.

---

### LO-10 — `unsafe.Pointer` aliasing in `float64ToBytes`

| File | `Common/Client/go/wshub/hub.go:588-594` |
| Severity | Low |

`unsafe.Slice((*byte)(unsafe.Pointer(&f[0])), len(f)*8)` aliases the float64 backing array. Currently safe (synchronous copy), but fragile if a caller retains the byte slice.

---

### LO-11 — `+Inf` in JSON from division by zero

| File | `Common/Client/go/wshub/stats.go:115-116` |
| Severity | Low |

`si.RateHz = 1.0 / avg` produces `+Inf` if `avg == 0`, yielding invalid JSON.

---

### LO-12 — Directory listing enabled

| File | `Client/webui/main.go:26` |
| Severity | Low |

`http.FileServer(http.Dir(*static))` serves directory listings if a directory lacks `index.html`.

---

### LO-13 — No security headers on static file serving

| File | `Client/debugger/main.go:55` |
| Severity | Low |

No `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options: nosniff`.

---

### LO-14 — `stopCh` close pattern not `sync.Once`-guarded

| File | `Client/debugger/martecontrol.go:182-189` |
| Severity | Low |

`select { case <-m.stopCh: default: close(m.stopCh) }` is not atomic; concurrent `Disconnect` calls would double-close and panic.

---

### LO-15 — `host_`/`port_` read by recv thread, written by UI thread

| File | `Client/streamhub/WSClient.cpp:48-58, 73-89, 160` |
| Severity | Low |

`std::string` concurrent read/write is technically UB (small window; recv thread re-reads on each reconnect cycle).

---

### LO-16 — `ReadExactTCP` progress edge case

| File | `Source/Components/Interfaces/UDPStream/UDPSClient.cpp:474-487` |
| Severity | Low |

If `Read` returns `true` but leaves `chunk` unchanged (contract violation), `got` would advance without data being read. Depends on MARTe2 `BasicTCPSocket::Read` contract.

---

### LO-17 — `bufMutex.Create(false)` return value unchecked

| Files | `Source/Components/DataSources/UDPStreamer/UDPStreamer.cpp:119`; `Source/Components/DataSources/UDPStreamerClient/UDPStreamerClient.cpp:149` |
| Severity | Low |

If mutex creation fails, all subsequent `FastLock` calls operate on an uninitialized semaphore → UB.

---

### LO-18 — No validation of `RangeMin < RangeMax` for quantization

| File | `Source/Components/DataSources/UDPStreamer/UDPStreamer.cpp:403-404` |
| Severity | Low |

If `rangeMax < rangeMin`, quantization normalization produces negative values clamped to 0.0 → silent data corruption. Divide-by-zero is guarded (`if (rRange == 0.0) rRange = 1.0`).

---

### LO-19 — Reassembler GC ticker panics if `expiry == 0`

| File | `Common/Client/go/udpsprotocol/reassembler.go:93` |
| Severity | Low |

`time.NewTicker(r.expiry / 2)` panics if `r.expiry == 0`. Currently always called with `2 * time.Second`.

---

## Cross-cutting themes

1. **Integer overflow in size arithmetic is pervasive** — every binary protocol parser (C++ producer, C++ consumer, Go decoder, C++ client wire layer) multiplies attacker-controlled counts by element sizes in 32-bit. Uniform fix: validate count against `len(payload)/elementSize` before multiplying; use 64-bit for bounds checks.
2. **No authentication anywhere in the control plane** — DebugService TCP, TcpLogger TCP, both Go web UIs, the C++ StreamHub WS, and the UDPS CONNECT protocol all trust any peer. Bind to localhost by default; document the trust boundary.
3. **WebSocket Origin checks disabled in every WS server** — Go `CheckOrigin: true`; C++ no Origin parse. Drive-by CSRF on all of them.
4. **`volatile` used for cross-thread synchronization** — TraceRingBuffer, TcpLogger queue. Not a memory-ordering primitive on non-x86; use atomics.
5. **`strstr`/`snprintf`-based JSON** in C++ clients — fragile against crafted-but-valid JSON; migrate to a real parser.
