FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

`HttpObjectEncoder` / `DefaultHttp2FrameWriter`: fix buffer leak when a `Throwable` is thrown during header encoding by HwangRock · Pull Request #17089 · netty/netty · GitHub

/ netty Public
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .java  (4) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -331,19 +331,26 @@ private void encodeFullHttpMessage(ChannelHandlerContext ctx, Object o, List<Obj
final int headersAndContentSize = (int) headersEncodedSizeAccumulator +
(accountForContentSize? content.readableBytes() : 0);
final ByteBuf buf = ctx.alloc().buffer(headersAndContentSize);
boolean handedOff = false;
try {
encodeInitialLine(buf, m);

encodeInitialLine(buf, m);

sanitizeHeadersBeforeEncode(m, state == ST_CONTENT_ALWAYS_EMPTY);
sanitizeHeadersBeforeEncode(m, state == ST_CONTENT_ALWAYS_EMPTY);

encodeHeaders(m.headers(), buf);
ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
encodeHeaders(m.headers(), buf);
ByteBufUtil.writeShortBE(buf, CRLF_SHORT);

// don't consider the copyContent case here: the statistics is just related the headers
headersEncodedSizeAccumulator = HEADERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
HEADERS_WEIGHT_HISTORICAL * headersEncodedSizeAccumulator;
// don't consider the copyContent case here: the statistics is just related the headers
headersEncodedSizeAccumulator = HEADERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
HEADERS_WEIGHT_HISTORICAL * headersEncodedSizeAccumulator;

encodeByteBufHttpContent(state, ctx, buf, content, msg.trailingHeaders(), out);
handedOff = true;
encodeByteBufHttpContent(state, ctx, buf, content, msg.trailingHeaders(), out);
} finally {
if (!handedOff) {
buf.release();
}
}
} finally {
msg.release();
}
Expand Down Expand Up @@ -521,19 +528,27 @@ private ByteBuf encodeInitHttpMessage(ChannelHandlerContext ctx, H m) throws Exc
assert state == ST_INIT;

ByteBuf buf = ctx.alloc().buffer((int) headersEncodedSizeAccumulator);
// Encode the message.
encodeInitialLine(buf, m);
state = isContentAlwaysEmpty(m) ? ST_CONTENT_ALWAYS_EMPTY :
HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK;
boolean success = false;
try {
// Encode the message.
encodeInitialLine(buf, m);
state = isContentAlwaysEmpty(m) ? ST_CONTENT_ALWAYS_EMPTY :
HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK;

sanitizeHeadersBeforeEncode(m, state == ST_CONTENT_ALWAYS_EMPTY);
sanitizeHeadersBeforeEncode(m, state == ST_CONTENT_ALWAYS_EMPTY);

encodeHeaders(m.headers(), buf);
ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
encodeHeaders(m.headers(), buf);
ByteBufUtil.writeShortBE(buf, CRLF_SHORT);

headersEncodedSizeAccumulator = HEADERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
HEADERS_WEIGHT_HISTORICAL * headersEncodedSizeAccumulator;
return buf;
headersEncodedSizeAccumulator = HEADERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
HEADERS_WEIGHT_HISTORICAL * headersEncodedSizeAccumulator;
success = true;
return buf;
} finally {
if (!success) {
buf.release();
}
}
}

/**
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,31 @@
*/
package io.netty.handler.codec.http;

import io.netty.buffer.AbstractByteBufAllocator;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.FileRegion;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.EncoderException;
import io.netty.util.CharsetUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;

import java.io.IOException;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class HttpResponseEncoderTest {
Expand Down Expand Up @@ -399,4 +411,156 @@ private static void testStatusResetContentTransferContentLength0(CharSequence he
assertEquals(responseText.toString(), written.toString());
assertFalse(channel.finish());
}

@Test
public void testInitHttpMessageHeaderEncodingFailureReleasesBuffer() throws Exception {
final TrackingFailingAllocator allocator = new TrackingFailingAllocator();
final EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
channel.config().setAllocator(allocator);

final DefaultHttpResponse response =
new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, new ThrowingHeaders());

ExecutionException e = assertThrows(ExecutionException.class, new Executable() {
@Override
public void execute() throws Throwable {
channel.writeAndFlush(response).get();
}
});
assertInstanceOf(EncoderException.class, e.getCause());
assertInstanceOf(OutOfMemoryError.class, e.getCause().getCause());

assertAllTrackedBuffersReleased(allocator);
channel.finishAndReleaseAll();
}

@Test
public void testFullHttpMessageHeaderEncodingFailureReleasesBuffer() throws Exception {
final TrackingFailingAllocator allocator = new TrackingFailingAllocator();
final EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
channel.config().setAllocator(allocator);

final DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER, new ThrowingHeaders(), new DefaultHttpHeaders());

ExecutionException e = assertThrows(ExecutionException.class, new Executable() {
@Override
public void execute() throws Throwable {
channel.writeAndFlush(response).get();
}
});
assertInstanceOf(EncoderException.class, e.getCause());
assertInstanceOf(OutOfMemoryError.class, e.getCause().getCause());

assertAllTrackedBuffersReleased(allocator);
channel.finishAndReleaseAll();
}

@Test
public void testChunkedContentLengthAllocationFailureDoesNotDoubleReleaseHeaderBuffer() throws Exception {
final TrackingFailingAllocator allocator = new TrackingFailingAllocator(3);
final EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
channel.config().setAllocator(allocator);

final DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK, Unpooled.copiedBuffer("1", CharsetUtil.US_ASCII));
HttpUtil.setTransferEncodingChunked(response, true);

EncoderException e = assertThrows(EncoderException.class, new Executable() {
@Override
public void execute() throws Throwable {
channel.writeOutbound(response);
}
});
assertInstanceOf(OutOfMemoryError.class, e.getCause());

for (;;) {
ByteBuf buf = channel.readOutbound();
if (buf == null) {
break;
}
buf.release();
}

assertAllTrackedBuffersReleased(allocator);
channel.finishAndReleaseAll();
}

private static void assertAllTrackedBuffersReleased(TrackingFailingAllocator allocator) {
assertFalse(allocator.allocated.isEmpty(), "expected at least one buffer to be allocated");
for (ByteBuf buf : allocator.allocated) {
assertEquals(0, buf.refCnt(), "expected tracked buffer to be released: " + buf);
}
}

/**
* A {@link ByteBufAllocator} that throws an {@link OutOfMemoryError} when asked to allocate a buffer with a
* given {@code initialCapacity}, and otherwise delegates to an unpooled allocator while tracking every
* successfully allocated buffer for leak verification.
*/
private static final class TrackingFailingAllocator extends AbstractByteBufAllocator {
private static final int NEVER_FAIL = -1;

private final ByteBufAllocator delegate = new UnpooledByteBufAllocator(false);
private final List<ByteBuf> allocated = new ArrayList<ByteBuf>();
private final int failOnInitialCapacity;

TrackingFailingAllocator() {
this(NEVER_FAIL);
}

TrackingFailingAllocator(int failOnInitialCapacity) {
super(false);
this.failOnInitialCapacity = failOnInitialCapacity;
}

private void failIfTargeted(int initialCapacity) {
if (failOnInitialCapacity != NEVER_FAIL && initialCapacity == failOnInitialCapacity) {
throw new OutOfMemoryError("simulated allocation failure for capacity " + initialCapacity);
}
}

@Override
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
failIfTargeted(initialCapacity);
ByteBuf buf = delegate.heapBuffer(initialCapacity, maxCapacity);
allocated.add(buf);
return buf;
}

@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
failIfTargeted(initialCapacity);
ByteBuf buf = delegate.directBuffer(initialCapacity, maxCapacity);
allocated.add(buf);
return buf;
}

@Override
public boolean isDirectBufferPooled() {
return delegate.isDirectBufferPooled();
}
}

private static final class ThrowingHeaders extends DefaultHttpHeaders {
@Override
public Iterator<Entry<CharSequence, CharSequence>> iteratorCharSequence() {
return new Iterator<Entry<CharSequence, CharSequence>>() {
@Override
public boolean hasNext() {
return true;
}

@Override
public Entry<CharSequence, CharSequence> next() {
throw new OutOfMemoryError("simulated header encoding failure");
}

@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
}
Loading
Loading

Back | FazBrowse Home | New Git URL