77 lines
2.6 KiB
Markdown
77 lines
2.6 KiB
Markdown
# phoenix-0001: CWE-407 — channel dispatch `event in event_intercepts` O(n) list scan per subscriber
|
||
|
||
| Field | Value |
|
||
|-------------|-------|
|
||
| ID | phoenix-0001 |
|
||
| Project | phoenixframework/phoenix |
|
||
| Severity | HIGH |
|
||
| Status | OPEN |
|
||
| CWE | CWE-407 (Algorithmic Complexity — Inefficient Algorithmic Complexity) |
|
||
| Found | 2026-03-27 |
|
||
|
||
## Location
|
||
|
||
`lib/phoenix/channel/server.ex:100`
|
||
|
||
```elixir
|
||
def dispatch(subscribers, from, %Broadcast{event: event} = msg) do
|
||
Enum.reduce(subscribers, %{}, fn
|
||
{pid, _}, cache when pid == from ->
|
||
cache
|
||
|
||
{pid, {:fastlane, fastlane_pid, serializer, event_intercepts}}, cache ->
|
||
if event in event_intercepts do # <-- CWE-407: O(n) list scan
|
||
```
|
||
|
||
## Root Cause
|
||
|
||
`event_intercepts` is populated at channel join time from `channel.__intercepts__()`, which
|
||
returns `@phoenix_intercepts` — a plain Elixir list accumulated at compile time:
|
||
|
||
```elixir
|
||
# lib/phoenix/channel.ex:456,488,522-525
|
||
@phoenix_intercepts []
|
||
def __intercepts__, do: @phoenix_intercepts
|
||
|
||
defmacro intercept(events) do
|
||
quote do: @phoenix_intercepts unquote(events)
|
||
end
|
||
```
|
||
|
||
The `dispatch/3` function is called once per broadcast event and iterates **every subscriber**
|
||
via `Enum.reduce/3`. For each fastlane subscriber it evaluates `event in event_intercepts`,
|
||
which is `List.member?/2` — O(k) where k is the number of intercepted events.
|
||
|
||
Total complexity per broadcast: **O(subscribers × intercepts)**.
|
||
|
||
## Impact
|
||
|
||
In a production Phoenix Channels deployment with N subscribers and K intercepted events, every
|
||
`broadcast/3` call costs O(N×K) membership tests. For a chat room with 10,000 subscribers and
|
||
5 intercepted events, this is 50,000 linear scans per broadcast message.
|
||
|
||
Channels are the highest-throughput path in Phoenix. Real-time applications (LiveView presence,
|
||
multiplayer games, chat) broadcast frequently. This is a genuine hot-path defect.
|
||
|
||
## Fix
|
||
|
||
Store `event_intercepts` as a `MapSet` at subscribe time:
|
||
|
||
```elixir
|
||
# lib/phoenix/channel/server.ex:443 — change to MapSet
|
||
fastlane = {:fastlane, transport_pid, serializer, MapSet.new(channel.__intercepts__())}
|
||
|
||
# lib/phoenix/channel/server.ex:100 — already uses `in`, MapSet.member? is called automatically
|
||
if event in event_intercepts do # MapSet.member? is O(1) hash lookup
|
||
```
|
||
|
||
The `in` operator in Elixir dispatches to `Enumerable.member?/2`, which for `MapSet` is O(1).
|
||
No change to line 100 is needed — only the construction at line 443.
|
||
|
||
## Patch
|
||
|
||
`defects/phoenix/patch/phoenix-0001-channel-dispatch-event-intercepts-mapset.patch`
|
||
|
||
## Benchmark
|
||
|
||
`defects/phoenix/unit/PhoenixTest.java`
|