Summary
Severity: High
Affected commit: f77d939
Raised via: PR #323, requested by @leynos
An unauthenticated remote memory-leak / availability bug exists in the Wireframe handshake context handoff introduced in commit f77d939. Registry insertion happens before a fallible network write, whilst cleanup is only performed by app construction on the normal successful path.
Background
Commit f77d939 replaces thread-local handshake storage with a process-global Tokio task-ID-keyed HashMap used to bridge the asynchronous handshake callback and the synchronous app factory.
scope_current_context(Some(...)) inserts the connection context into this global registry before the scoped future is polled, but never removes the entry when the future returns an error. In the handshake success path that scoped future writes the 8-byte success reply and only later reaches the app-factory path that consumes the registry entry via take_current_context(). If a remote client sends a valid 12-byte TRTP preamble and then closes/resets the socket so write_handshake_reply(...).await? returns an I/O error, the connection setup aborts before the app factory can run, leaving the registry entry permanently retained. Repeating this pre-authentication flow can grow the global HashMap without bound and exhaust server memory.
The previous thread-local implementation did not create an unbounded per-task global map; stale state was at most per worker thread and overwritten.
Attack Path
- Remote unauthenticated TCP client connects to the Wireframe listener on the configured bind address.
- Client sends a valid 12-byte TRTP/version-1 preamble.
HotlinePreamble validation succeeds; the success callback fires.
scope_current_context inserts ConnectionContext into the global task-ID registry.
write_handshake_reply fails because the client reset/closed the connection.
- The error aborts setup; the app factory is never reached.
take_current_context() is never called; the registry entry is retained.
- Steps 1–7 are repeated without bound, exhausting process memory.
Evidence
src/wireframe/connection.rs
- Lines 116–130 —
registry() introduces a process-global HashMap<Id, ConnectionContext>. Entries persist until explicitly removed.
- Lines 154–164 —
scope_current_context stores the context in the global registry before awaiting the scoped future, but provides no finally/drop cleanup when that future returns an error.
- Lines 193–201 —
take_current_context() is the only removal path; it is reached by the app factory on the normal setup path, not when handshake setup aborts before app construction.
src/wireframe/handshake.rs (lines 79–82)
scope_current_context is called with the cloned context before write_handshake_reply(...).await?. If the client disconnects or resets, the write fails, the function exits with an error, and the registry entry is never consumed.
src/server/wireframe/mod.rs (lines 190–200)
build_app_context calls take_current_context() to consume and remove the stored context. This function is only invoked after successful handshake setup; aborted handshakes do not reach this cleanup path.
Impact
| Dimension |
Assessment |
| Likelihood |
High — requires only network reachability, a valid 12-byte TRTP preamble, and a client-controlled reset. Pre-authentication; default bind is all interfaces on TCP 5500. |
| Impact |
High — unbounded process-global map growth; repeated unauthenticated connections exhaust daemon/host memory (DoS). No RCE, data disclosure, or authentication bypass. |
| Severity |
High |
Existing Controls
- Preamble validation requires the TRTP protocol ID and supported version before the vulnerable path.
- A 5-second preamble timeout limits idle sockets but does not clean entries inserted after a valid preamble.
- Authentication and authorisation are not reached before the vulnerable handshake path.
- Deployments using only the legacy runtime are not affected.
Suggested Remediation Direction
Ensure scope_current_context removes the registry entry unconditionally when the scoped future completes, whether with success or error. Introduce a drop-guard that calls take_registry_context() when it goes out of scope, so the guard fires on both the success and error paths.
Assumptions / Blindspots
- The external
wireframe crate implementation was not inspected from source in this static pass; reachability relies on repository server construction and comments about preamble callbacks and app factory ordering.
- No deployment manifests confirm whether
mxd-wireframe-server is exposed publicly or sits behind a private proxy.
- The exact number of aborted handshakes needed for host-level exhaustion depends on allocator behaviour, Tokio task-ID behaviour, memory limits, and operational connection controls outside the repository.
Summary
Severity: High
Affected commit: f77d939
Raised via: PR #323, requested by @leynos
An unauthenticated remote memory-leak / availability bug exists in the Wireframe handshake context handoff introduced in commit f77d939. Registry insertion happens before a fallible network write, whilst cleanup is only performed by app construction on the normal successful path.
Background
Commit f77d939 replaces thread-local handshake storage with a process-global Tokio task-ID-keyed
HashMapused to bridge the asynchronous handshake callback and the synchronous app factory.scope_current_context(Some(...))inserts the connection context into this global registry before the scoped future is polled, but never removes the entry when the future returns an error. In the handshake success path that scoped future writes the 8-byte success reply and only later reaches the app-factory path that consumes the registry entry viatake_current_context(). If a remote client sends a valid 12-byte TRTP preamble and then closes/resets the socket sowrite_handshake_reply(...).await?returns an I/O error, the connection setup aborts before the app factory can run, leaving the registry entry permanently retained. Repeating this pre-authentication flow can grow the globalHashMapwithout bound and exhaust server memory.The previous thread-local implementation did not create an unbounded per-task global map; stale state was at most per worker thread and overwritten.
Attack Path
HotlinePreamblevalidation succeeds; the success callback fires.scope_current_contextinsertsConnectionContextinto the global task-ID registry.write_handshake_replyfails because the client reset/closed the connection.take_current_context()is never called; the registry entry is retained.Evidence
src/wireframe/connection.rsregistry()introduces a process-globalHashMap<Id, ConnectionContext>. Entries persist until explicitly removed.scope_current_contextstores the context in the global registry before awaiting the scoped future, but provides no finally/drop cleanup when that future returns an error.take_current_context()is the only removal path; it is reached by the app factory on the normal setup path, not when handshake setup aborts before app construction.src/wireframe/handshake.rs(lines 79–82)scope_current_contextis called with the cloned context beforewrite_handshake_reply(...).await?. If the client disconnects or resets, the write fails, the function exits with an error, and the registry entry is never consumed.src/server/wireframe/mod.rs(lines 190–200)build_app_contextcallstake_current_context()to consume and remove the stored context. This function is only invoked after successful handshake setup; aborted handshakes do not reach this cleanup path.Impact
Existing Controls
Suggested Remediation Direction
Ensure
scope_current_contextremoves the registry entry unconditionally when the scoped future completes, whether with success or error. Introduce a drop-guard that callstake_registry_context()when it goes out of scope, so the guard fires on both the success and error paths.Assumptions / Blindspots
wireframecrate implementation was not inspected from source in this static pass; reachability relies on repository server construction and comments about preamble callbacks and app factory ordering.mxd-wireframe-serveris exposed publicly or sits behind a private proxy.