Skip to content

Commit b72cf26

Browse files
Cap pre-join plugin-message queue size (prevents arbitrary growth/OOM) (#1800)
* Cap pre-join plugin-message queue size (prevents arbitrary growth/OOM) * Clear counters once as the entire queue will have been processed
1 parent 7d68208 commit b72cf26

2 files changed

Lines changed: 50 additions & 3 deletions

File tree

proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
import java.util.UUID;
8787
import java.util.concurrent.CompletableFuture;
8888
import java.util.concurrent.ConcurrentLinkedQueue;
89+
import java.util.concurrent.atomic.AtomicInteger;
90+
import java.util.concurrent.atomic.AtomicLong;
8991
import net.kyori.adventure.key.Key;
9092
import net.kyori.adventure.text.Component;
9193
import net.kyori.adventure.text.ComponentLike;
@@ -102,12 +104,23 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
102104
private static final boolean BACKPRESSURE_LOG =
103105
Boolean.getBoolean("velocity.log-server-backpressure");
104106

107+
// Caps the per-connection queue used while the FML/login phases are not yet "complete". Without
108+
// these caps, a client that never completes its handshake phase can spam plugin messages (each up
109+
// to ~32 KiB serverbound) and grow the queue without bound.
110+
private static final long MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES =
111+
Long.getLong("velocity.max-queued-login-plugin-message-bytes", 4L * 1024 * 1024);
112+
private static final int MAX_QUEUED_LOGIN_PLUGIN_MESSAGES =
113+
Integer.getInteger("velocity.max-queued-login-plugin-messages", 1024);
114+
105115
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
106116

107117
private final ConnectedPlayer player;
108118
private boolean spawned = false;
109119
private final List<UUID> serverBossBars = new ArrayList<>();
110120
private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>();
121+
private final AtomicLong loginPluginMessagesBytes = new AtomicLong();
122+
private final AtomicInteger loginPluginMessagesCount = new AtomicInteger();
123+
private volatile boolean loginPluginMessagesOverflowed;
111124
private final VelocityServer server;
112125
private @Nullable TabCompleteRequestPacket outstandingTabComplete;
113126
private final ChatHandler<? extends MinecraftPacket> chatHandler;
@@ -176,9 +189,38 @@ public void activated() {
176189
@Override
177190
public void deactivated() {
178191
player.discardChatQueue();
179-
for (PluginMessagePacket message : loginPluginMessages) {
192+
PluginMessagePacket message;
193+
while ((message = loginPluginMessages.poll()) != null) {
180194
ReferenceCountUtil.release(message);
181195
}
196+
loginPluginMessagesBytes.set(0);
197+
loginPluginMessagesCount.set(0);
198+
}
199+
200+
/**
201+
* Adds a retained plugin message to the queue used while the FML/login phases are still in
202+
* progress, enforcing the per-connection byte and count caps. Returns {@code true} if queued,
203+
* {@code false} if the packet was released (and the player disconnected on overflow).
204+
*/
205+
private boolean enqueueLoginPluginMessage(PluginMessagePacket packet) {
206+
if (loginPluginMessagesOverflowed) {
207+
ReferenceCountUtil.release(packet);
208+
return false;
209+
}
210+
int packetSize = packet.content().readableBytes();
211+
long newBytes = loginPluginMessagesBytes.addAndGet(packetSize);
212+
int newCount = loginPluginMessagesCount.incrementAndGet();
213+
if (newBytes > MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES
214+
|| newCount > MAX_QUEUED_LOGIN_PLUGIN_MESSAGES) {
215+
loginPluginMessagesOverflowed = true;
216+
ReferenceCountUtil.release(packet);
217+
logger.warn("Disconnecting {}: pre-join plugin-message queue exceeded its limits "
218+
+ "({} messages, {} bytes).", player, newCount, newBytes);
219+
player.disconnect(Component.translatable("velocity.error.plugin-message-overflow"));
220+
return false;
221+
}
222+
loginPluginMessages.add(packet);
223+
return true;
182224
}
183225

184226
@Override
@@ -359,7 +401,7 @@ public boolean handle(PluginMessagePacket packet) {
359401
//
360402
// We also need to make sure to retain these packets, so they can be flushed
361403
// appropriately.
362-
loginPluginMessages.add(packet.retain());
404+
enqueueLoginPluginMessage(packet.retain());
363405
} else {
364406
// The connection is ready, send the packet now.
365407
backendConn.write(packet.retain());
@@ -374,7 +416,7 @@ public boolean handle(PluginMessagePacket packet) {
374416
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
375417
.consideredComplete()) {
376418
// We're still processing the connection (see above), enqueue the packet for now.
377-
loginPluginMessages.add(message.retain());
419+
enqueueLoginPluginMessage(message.retain());
378420
} else {
379421
backendConn.write(message);
380422
}
@@ -637,6 +679,8 @@ public void handleBackendJoinGame(JoinGamePacket joinGame, VelocityServerConnect
637679
while ((pm = loginPluginMessages.poll()) != null) {
638680
serverMc.delayedWrite(pm);
639681
}
682+
loginPluginMessagesBytes.set(0);
683+
loginPluginMessagesCount.set(0);
640684

641685
// Clear any title from the previous server.
642686
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
@@ -869,6 +913,8 @@ public void flushQueuedMessages() {
869913
while ((pm = loginPluginMessages.poll()) != null) {
870914
connection.write(pm);
871915
}
916+
loginPluginMessagesBytes.set(0);
917+
loginPluginMessagesCount.set(0);
872918
}
873919
}
874920
}

proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ velocity.error.internal-server-connection-error=An internal server connection er
2525
velocity.error.logging-in-too-fast=You are logging in too fast, try again later.
2626
velocity.error.online-mode-only=You are not logged into your Minecraft account. If you are logged into your Minecraft account, try restarting your Minecraft client.
2727
velocity.error.player-connection-error=An internal error occurred in your connection.
28+
velocity.error.plugin-message-overflow=You sent too many plugin messages before completing the connection.
2829
velocity.error.modern-forwarding-needs-new-client=This server is only compatible with Minecraft 1.13 and above.
2930
velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding.
3031
velocity.error.moved-to-new-server=You were kicked from <arg:0>: <arg:1>

0 commit comments

Comments
 (0)