40 KiB
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:
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:
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:
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):
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:
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:
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):
// 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:
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:
// 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:
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:
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):
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):
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 deletes 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:
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:
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:
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
DebugServiceis destroyed, patched builders remain. - A second
DebugServiceinstance double-patches; the first's builders are leaked. - Any
dynamic_castto 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:
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:
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) |
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 |
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 |
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 |
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 |
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 |
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
CONTINUATIONopcode 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).
CLOSEframe 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 |
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
- 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)/elementSizebefore multiplying; use 64-bit for bounds checks. - 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.
- WebSocket Origin checks disabled in every WS server — Go
CheckOrigin: true; C++ no Origin parse. Drive-by CSRF on all of them. volatileused for cross-thread synchronization — TraceRingBuffer, TcpLogger queue. Not a memory-ordering primitive on non-x86; use atomics.strstr/snprintf-based JSON in C++ clients — fragile against crafted-but-valid JSON; migrate to a real parser.