Skip to content

Commit 5eb7065

Browse files
committed
Fix remote OOM via delayed deserialization for unary/server-streaming calls
Protobuf unknown-field or repeated field amplification can lead to remote OOM if an attacker sends a unary request but holds the stream open without half-closing. This change delays the deserialization of incoming messages for calls where the client sends at most one message (Unary and Server Streaming) until the client actually half-closes the stream (sends END_STREAM). If the call is cancelled before half-close, the buffered raw message is discarded without being deserialized, preventing the memory explosion.
1 parent e9a8c2b commit 5eb7065

2 files changed

Lines changed: 287 additions & 16 deletions

File tree

core/src/main/java/io/grpc/internal/ServerCallImpl.java

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.grpc.CompressorRegistry;
3535
import io.grpc.Context;
3636
import io.grpc.DecompressorRegistry;
37+
import io.grpc.Detachable;
3738
import io.grpc.InternalDecompressorRegistry;
3839
import io.grpc.InternalStatus;
3940
import io.grpc.Metadata;
@@ -45,6 +46,9 @@
4546
import io.perfmark.PerfMark;
4647
import io.perfmark.Tag;
4748
import io.perfmark.TaskCloseable;
49+
import java.io.ByteArrayInputStream;
50+
import java.io.ByteArrayOutputStream;
51+
import java.io.IOException;
4852
import java.io.InputStream;
4953
import java.util.logging.Level;
5054
import java.util.logging.Logger;
@@ -288,6 +292,7 @@ static final class ServerStreamListenerImpl<ReqT> implements ServerStreamListene
288292
private final ServerCallImpl<ReqT, ?> call;
289293
private final ServerCall.Listener<ReqT> listener;
290294
private final Context.CancellableContext context;
295+
private InputStream delayedMessage;
291296

292297
public ServerStreamListenerImpl(
293298
ServerCallImpl<ReqT, ?> call, ServerCall.Listener<ReqT> listener,
@@ -320,6 +325,20 @@ public void messagesAvailable(MessageProducer producer) {
320325
}
321326
}
322327

328+
private static InputStream bufferMessage(InputStream is) throws IOException {
329+
if (is instanceof Detachable) {
330+
return ((Detachable) is).detach();
331+
}
332+
// Fallback: copy to byte array
333+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
334+
byte[] buffer = new byte[4096];
335+
int bytesRead;
336+
while ((bytesRead = is.read(buffer)) != -1) {
337+
baos.write(buffer, 0, bytesRead);
338+
}
339+
return new ByteArrayInputStream(baos.toByteArray());
340+
}
341+
323342
@SuppressWarnings("Finally") // The code avoids suppressing the exception thrown from try
324343
private void messagesAvailableInternal(final MessageProducer producer) {
325344
if (call.cancelled) {
@@ -330,13 +349,32 @@ private void messagesAvailableInternal(final MessageProducer producer) {
330349
InputStream message;
331350
try {
332351
while ((message = producer.next()) != null) {
333-
try {
334-
listener.onMessage(call.method.parseRequest(message));
335-
} catch (Throwable t) {
336-
GrpcUtil.closeQuietly(message);
337-
throw t;
352+
if (call.method.getType().clientSendsOneMessage()) {
353+
if (delayedMessage != null) {
354+
GrpcUtil.closeQuietly(message);
355+
call.close(
356+
Status.INTERNAL.withDescription("Too many requests"),
357+
new Metadata());
358+
GrpcUtil.closeQuietly(delayedMessage);
359+
delayedMessage = null;
360+
return;
361+
}
362+
try {
363+
delayedMessage = bufferMessage(message);
364+
} catch (Throwable t) {
365+
GrpcUtil.closeQuietly(message);
366+
throw t;
367+
}
368+
message.close();
369+
} else {
370+
try {
371+
listener.onMessage(call.method.parseRequest(message));
372+
} catch (Throwable t) {
373+
GrpcUtil.closeQuietly(message);
374+
throw t;
375+
}
376+
message.close();
338377
}
339-
message.close();
340378
}
341379
} catch (Throwable t) {
342380
GrpcUtil.closeQuietly(producer);
@@ -353,6 +391,23 @@ public void halfClosed() {
353391
return;
354392
}
355393

394+
if (delayedMessage != null) {
395+
InputStream message = delayedMessage;
396+
delayedMessage = null;
397+
try {
398+
listener.onMessage(call.method.parseRequest(message));
399+
} catch (Throwable t) {
400+
GrpcUtil.closeQuietly(message);
401+
Throwables.throwIfUnchecked(t);
402+
throw new RuntimeException(t);
403+
}
404+
try {
405+
message.close();
406+
} catch (IOException e) {
407+
throw new RuntimeException(e);
408+
}
409+
}
410+
356411
listener.onHalfClose();
357412
}
358413
}
@@ -366,6 +421,10 @@ public void closed(Status status) {
366421
}
367422

368423
private void closedInternal(Status status) {
424+
if (delayedMessage != null) {
425+
GrpcUtil.closeQuietly(delayedMessage);
426+
delayedMessage = null;
427+
}
369428
Throwable cancelCause = null;
370429
try {
371430
if (status.isOk()) {

0 commit comments

Comments
 (0)