# Binary Hub Protocol (wire) — Known Issues

For planned/actionable work see `SIGNALR_BINARY_PROTOCOL_TODO.md`.
For higher-level SignalR abstractions see `../SIGNALR/SIGNALR_ISSUES.md`.

## ACCORE-SBP-I-T3W9: AsyncSegment blocking waits on thread-pool → starvation deadlock under concurrent load

**Severity:** Critical · **Status:** Partially Fixed (deser-side fixed 2026-06-08; send-side TBD) · **Area:** `AcBinaryHubProtocol` AsyncSegment send + receive paths

### Description

In `AsyncSegment` mode both directions do **synchronous blocking waits on .NET thread-pool threads**, which starve the pool under concurrent load + real network latency (production-only; never reproduces locally where there is no network and one user). Symptom: clients stuck loading → ~60 s timeout. Hits **all** client types (incl. WASM, which has no `.Wait` of its own — they are victims of server-side starvation).

- **Receive (deser):** `TryParseChunkData` runs the streaming deser via `Task.Run`, which blocks in `AsyncPipeReaderInput.TryAdvanceSegment` → `ManualResetEventSlim.Wait()` between network-delivered chunks. The unblocking `Feed()`+`Set()` runs on the SignalR receive-continuation — **also** a pool thread → circular dependency → pool starvation, broken only by the runtime's slow thread injection (~1-2/s).
- **Send (flush):** `WriteMessageChunked` → `SyncFlush(pipeWriter.FlushAsync())` (lines ~535, ~1135) blocks a dispatch (pool) thread **per chunk** under client backpressure (slow mobile clients) while streaming multi-MB responses. Forced by the synchronous `IHubProtocol.WriteMessage` API. **This is the dominant production trigger** (proven by the WASM-client hang + multi-MB server→client payloads).

`Bytes` mode avoids both (no background deser, no per-chunk sync flush) — hence stable as the current hotfix.

### Fix — deser (receive) side ✅ Fixed (2026-06-08)

Resolved — `TryParseChunkData` runs the blocking streaming deser off the .NET thread pool via `AcBinaryDeserExecutor` (`AyCode.Services/SignalRs/AcBinaryDeserExecutor.cs`). See that file's doc-comment for the design.

### Fix — send (flush) side

**TBD.** Per-chunk `SyncFlush` blocks the dispatch thread under client backpressure.

**Why it's harder than the deser side:** the blocking sits inside `IHubProtocol.WriteMessage` (synchronous, called by SignalR on a pool thread, must return when the send is done). Unlike the receive side — where *we* spawned the deser task and could move it off-pool — we don't own the calling thread here, so the "off-pool dedicated thread" trick does not apply. Within the sync `WriteMessage` contract the choice is binary: flush per chunk (bounded memory, **blocks**) or buffer the whole message + one async flush (no block, **multi-MB memory** = effectively Bytes). "Bounded memory + no block" is not reachable inside `WriteMessage`.

**Possible solutions:**
- **A) Per-item SignalR native streaming** (existing `StreamAllAsync` ↔ `OnReceiveStreamMessage` path) — async backpressure, no block. **Rejected: loses whole-graph dedup** — code-verified: `EnumerateGenericAsync<T>` serializes PER ITEM (`SignalRSerializationHelper.SerializeToBinary` per element = independent serialization context each), so cross-item idtracking + string-interning is lost.
- **B) Application-level batching** — N small Bytes messages, client reassembles. Same dedup loss across batches. Rejected for the same reason.
- **C) Mitigation only** — `FlushPolicy.Coalesced` + lower `FlushTimeout` reduce blocking frequency but do not eliminate it under sustained backpressure. Band-aid.
- **D) Stay on Bytes** — no block, but multi-MB byte[] (LOH) per send, no streaming. Current production hotfix; not the target state.
- **E) Whole-graph chunk-stream over SignalR native streaming — leading candidate (code-verified design; prototype pending).**
  - **Server**: a dedicated off-pool thread (mirror of `AcBinaryDeserExecutor`) runs the **existing** `AcBinarySerializer.SerializeChunked(value, pipe, options)` (Pipe overload, `AcBinarySerializer.cs:527`; **raw mode** — avoids the framed-mode 65 KB single-value cap `ACCORE-BIN-I-B7K9`) into an internal `Pipe`. This is ONE serialize pass (single `ScanForDuplicates` + one context) → **whole-graph idtracking + string-interning preserved**. Its backpressure block (`SyncAwaitFlush` → `Task.Wait`, `AsyncPipeWriterOutput.cs:207-217`) lands on the dedicated thread, NOT the pool. `OnReceiveStreamMessage` async-reads `pipe.Reader` and yields `byte[]` chunks → SignalR native streaming carries them with async backpressure. The byte[] StreamItems bypass the chunked `WriteMessage`/`SyncFlush` path entirely (`HasStreamableArgs` returns false for byte[], `AcBinaryHubProtocol.cs:408`).
  - **Client**: NEW consumer (today `StreamAllAsync` deserializes each chunk as a complete message — reassembly does not exist yet): `await foreach` feeds chunks into one `AsyncPipeReaderInput(multiMessage: false)` (raw verbatim mode, `AsyncPipeReaderInput.cs:181,218-223`), the whole-graph deserialize runs off-pool via `AcBinaryDeserExecutor` (same assembly), join only AFTER `Complete()`. WASM: accumulate + synchronous deserialize on completion (mirror of the protocol's `IsBrowser` fallback).
  - **Open points (prototype/test before commit)**: client composition deadlock discipline (deser MUST be off-pool; the feed loop must never block on the deser task before `Complete()` — same bug class as the deser-side fix); WASM fallback wiring; `UseCompression` unsupported on the PipeWriter path; perf of the extra Pipe hop + dedicated send thread (benchmark vs Bytes).

### Related TODO

(To be opened when the deser-side fix lands and the send-side direction is committed.)

## ACCORE-SBP-I-F6T2: AsyncSegment send-path unsupported on WebAssembly

**Severity:** Major (on WASM) · **Status:** Open · **Area:** `AsyncPipeWriterOutput` / WASM runtime

### Description
`AsyncPipeWriterOutput.SyncAwaitFlush` uses `Task.Wait(timeout)` — deadlocks the single-threaded WASM UI thread. Therefore, WASM clients cannot SEND with `AsyncSegment` mode.

### Known workaround (multi-layer)
1. **`AcBinaryHubProtocolOptions.Validate()`** throws `PlatformNotSupportedException` if WASM + AsyncSegment combination is requested → prevents deadlock
2. **Consumer code-level safety-net** downgrades `AsyncSegment → Segment` on WASM (see `FruitBankHybrid.Web.Client/Program.cs` Configure lambda)
3. **Receive-path** on WASM is fully supported — `AsyncPipeReaderInput` with synchronous fallback at `CHUNK_END` (after `Complete()`, `TryAdvanceSegment` never enters the `MRES.Wait` path) means WASM clients CAN receive AsyncSegment-chunked data from a non-WASM sender, they just cannot send AsyncSegment themselves

### Related TODO
- [`SIGNALR_BINARY_PROTOCOL_WASMTHREADING.md`](SIGNALR_BINARY_PROTOCOL_WASMTHREADING.md) — documents the `WasmEnableThreads` upgrade path that would enable AsyncSegment SEND-path on WASM (doc-only, not implemented; .NET 10 stable / .NET 11 milestone).

## ACCORE-SBP-I-G4B5: StaticWebAssets SDK "Illegal characters" noise (consumer build)

**Severity:** Cosmetic (non-blocking) · **Status:** Open · **Area:** SDK, not our code

### Description
Consumer projects using `Microsoft.NET.Sdk.BlazorWebAssembly` may see "Illegal characters in path" errors in the VS design-time error list. Originates from the SDK's `DefineStaticWebAssets` task calling legacy `FileIOPermission.EmulateFileIOPermissionChecks`, which is stricter than NTFS (e.g., double spaces or certain path patterns in any wwwroot asset trigger it).

Build succeeds, app runs — the "error" is error-list-only.

### Known workaround
Ignore the error-list entry. If it becomes truly blocking, inspect wwwroot (including transitive from RCL projects) for problematic filenames (double spaces, control chars, reserved chars).

### Related TODO
None — upstream SDK fix required.

## ACCORE-SBP-I-R8K3: `Type.GetType(wire-supplied-name)` enables deserialization-gadget attack

**Severity:** Critical (security) · **Status:** Open · **Area:** `AyCodeBinaryHubProtocol` — argument type-binder

### Description

`AyCodeBinaryHubProtocol` writes the runtime type's `AssemblyQualifiedName` into the wire-format header for the `data` argument of an Invocation message, and the receiver resolves it via `Type.GetType(typeName)` before calling `AcBinaryDeserializer.Deserialize(arr, ..., resolvedType, options)`:

```csharp
// Sender (line ~92-98 in AyCodeBinaryHubProtocol.cs):
typeName = dataArg.GetType().AssemblyQualifiedName;
WriteNullableString(ref bw, typeName);

// Receiver (line ~112-114):
var typeName = ReadNullableString(ref r);
if (typeName != null)
    resolvedType = Type.GetType(typeName);

// Receiver (line ~214):
return AcBinaryDeserializer.Deserialize(arr, offset, length, resolvedType, Options);
```

**This is a textbook deserialization-gadget pattern.** A malicious / compromised client can send any `AssemblyQualifiedName` reachable from the server's loaded assemblies (BCL types like `System.Diagnostics.Process`, `System.IO.File`-related descriptor types, `Microsoft.Win32.Registry...` types, custom domain types containing dangerous setters, etc.) and trigger their construction + property-set during deserialization. This is exactly the attack class that caused `BinaryFormatter` to be deprecated and eventually removed from .NET — Microsoft's official guidance is to NEVER deserialize a type identified by a wire-supplied name.

Even types tagged `[AcBinarySerializable]` may have **side-effecting setters** that an attacker can chain to gadget-walk into RCE; the attribute is not a security boundary, just a serializer opt-in.

### Threat model

- **Attacker:** any party able to send a SignalR Invocation message with attacker-controlled `dataArg` type-name. In an authenticated SignalR setup this is "any logged-in user"; in an open hub, "anyone who can reach the endpoint".
- **Impact:** arbitrary type instantiation in the server's process. Concrete impact depends on which dangerous types are reachable in the server's loaded assemblies — ranges from information disclosure (constructors that read files / registry / config) to denial-of-service (large-allocation types) to remote code execution (gadget chains via setters that invoke delegates / reflection / Process.Start). The exact exploitability is application-dependent but the **attack surface is wide-open by default** with this pattern.
- **NOT in scope:** the `AcBinarySerializer` core itself does not have this problem — it deserializes a caller-supplied compile-time `T`. The vulnerability is **specific to `AyCodeBinaryHubProtocol`'s decision** to embed the type-name on the wire.

### Why the pattern was added

Originally the header type-name was needed because Hub method dispatch uses `IInvocationBinder` which resolves arg types from the registered method signatures, but for `object`-typed parameters (or generic data payloads) there's no compile-time type to bind to — the protocol therefore needs the runtime type-info on the wire to round-trip the `dataArg` correctly. **The mechanism is necessary; the implementation is unsafe.**

### Mitigations (any combination — defence in depth)

1. **Whitelist registry (preferred)** — replace the `Type.GetType(typeName)` lookup with a `Dictionary<string, Type>` (or `Dictionary<int, Type>` if also moving to integer tags) populated at server startup with only the application's known DTO types. Reject any `typeName` not in the whitelist. The registry can be auto-populated from `[AcBinarySerializable]`-marked types in registered assemblies if that's an acceptable trust boundary, or maintained by hand for stricter control.

2. **Tag-based dispatch (longer-term)** — replace the wire-format `AssemblyQualifiedName` (variable-length string, ~50-150 bytes) with a fixed-width type-tag (`int` or `varint`, 1-5 bytes). Sender and receiver share a `Dictionary<int, Type>` registry. Smaller wire footprint **and** safer (the tag space is application-defined, not BCL-reachable). Breaking change to the wire format — coordinate with `ACCORE-SBP-I-F6T2` and ongoing protocol evolution.

3. **Type-hash dispatch (compromise)** — keep variable-length lookup but use `FNV-1a(typeName)` as the wire payload (4 bytes) and validate against a server registry. Doesn't eliminate the registry but reduces wire overhead vs. full string.

4. **Authenticate-then-trust deferral** — until a real fix lands, audit the deployment for: (a) authenticated-only Hub access, (b) NO dangerous types reachable from the server's loaded assemblies (audit BCL types in particular). This is **not** a security mitigation, it's a "reduce blast radius" stopgap.

### Related TODO

- [`SIGNALR_BINARY_PROTOCOL_TODO.md#accore-sbp-t-s2h6`](SIGNALR_BINARY_PROTOCOL_TODO.md#accore-sbp-t-s2h6-replace-typegettypewire-supplied-name-with-whitelist-based-type-resolution) — implementation tracking (P0)

### Planned solution

See [`../../../../AyCode.Core/AyCode.Core/docs/BINARY/BINARY_AQN_IMPLEMENTATION.md`](../../../../AyCode.Core/AyCode.Core/docs/BINARY/BINARY_AQN_IMPLEMENTATION.md) — design plan that consolidates the discussion: single `IAcTypeBinder` shared between AcBinary polymorph and `AyCodeBinaryHubProtocol` HasType paths, implicit trust via `BinaryDeserializeTypeMetadata` ctor-feed, explicit `Allow<T>()` allowlist for non-SGen 3rd-party polymorph subtypes, framework helper `SignalRTagBindings.ExtractTypes` for bulk-seeding from `[SignalRTagBinding]` attributes.
