6
votes

I have a simple spring-boot application. It has a single endpoint that takes an object from the request body, and does nothing:

@Controller
class FooController {
    @RequestMapping(method=RequestMethod.POST, value="/foo")
    public void postFoo(@RequestBody Foo foo) {
    }
}

Pretty simple stuff.

I then connect via telnet and send through appropriate headers as if I'm about to send a json-encoded object, but never send the request body - I just leave the connection hanging.

Running jstack, I can see that tomcat has dispatched the request to spring. Spring has sent it to jackson. Jackson is blocked on NIO waiting for more data to come in.

Thread 12128: (state = BLOCKED)
 - sun.misc.Unsafe.park(boolean, long) @bci=0 (Compiled frame; information may be imprecise)
 - java.util.concurrent.locks.LockSupport.parkNanos(java.lang.Object, long) @bci=20, line=226 (Compiled frame)
 ...
 - org.apache.tomcat.util.net.NioEndpoint$KeyAttachment.awaitLatch(java.util.concurrent.CountDownLatch, long, java.util.concurrent.TimeUnit) @bci=18, line=1582 (Compiled frame)
 ...
 - org.apache.tomcat.util.net.NioSelectorPool.read(java.nio.ByteBuffer, org.apache.tomcat.util.net.NioChannel, java.nio.channels.Selector, long) @bci=7, line=227 (Compiled frame)
 - org.apache.coyote.http11.InternalNioInputBuffer.readSocket(boolean, boolean) @bci=103, line=427 (Compiled frame)
...
 - org.apache.catalina.connector.CoyoteInputStream.read(byte[], int, int) @bci=76, line=200 (Compiled frame)
 - com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.ensureLoaded(int) @bci=49, line=503 (Compiled frame)
 ...
 - com.fasterxml.jackson.databind.ObjectMapper.readValue(java.io.InputStream, com.fasterxml.jackson.databind.JavaType) @bci=6, line=2158 (Compiled frame)
 - org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.readJavaType(com.fasterxml.jackson.databind.JavaType, org.springframework.http.HttpInputMessage) @bci=11, line=225 (Compiled frame)
 ...
 - org.springframework.web.servlet.FrameworkServlet.doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) @bci=3, line=863 (Compiled frame)
 - javax.servlet.http.HttpServlet.service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) @bci=149, line=646 (Compiled frame)
 - org.springframework.web.servlet.FrameworkServlet.service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) @bci=32, line=837 (Compiled frame)
 - javax.servlet.http.HttpServlet.service(javax.servlet.ServletRequest, javax.servlet.ServletResponse) @bci=30, line=727 (Compiled frame)
 - org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse) @bci=446, line=303 (Compiled frame)
 ...
 - org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(java.nio.channels.SelectionKey, org.apache.tomcat.util.net.NioEndpoint$KeyAttachment) @bci=140, line=1736 (Interpreted frame)
 - org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run() @bci=94, line=1695 (Compiled frame)
 - java.util.concurrent.ThreadPoolExecutor.runWorker(java.util.concurrent.ThreadPoolExecutor$Worker) @bci=95, line=1145 (Compiled frame)
 - java.util.concurrent.ThreadPoolExecutor$Worker.run() @bci=5, line=615 (Interpreted frame)
 - org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run() @bci=4, line=61 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)

My problem is that if 200 people do that, then my thread pool gets starved, and legitimate requests can't come in. It seems this is a pretty simple DOS attack against anything running on tomcat.

I presume there's a way of working around this by getting the NIO HTTP Connector to read ahead into its buffer. If so, how would I go about setting this up?

Even then, it seems a malicious agent could easily bring a service down by just sending large objects through to it. How do people normally prevent thread starvation when slow, buggy or malicious clients are connecting?

1
Martin, I need you to post more of the stack. I have a feeling like that thread is locked on that CountdownLatch twice. All read/write operations from nonblocking sockets are... non blocking. Meaning a read() will return the data currently available and not wait for any more.Johnny V
@Martin: Did you get the resolution? Please provide the solution if anyone able to solve it.Bhanu Pasrija

1 Answers

0
votes

Check out the Stuck Thread Detection Valve in Tomcat.

From the docs:

This valve allows to detect requests that take a long time to process, which might indicate that the thread that is processing it is stuck. Additionally it can optionally interrupt such threads to try and unblock them.

When such a request is detected, the current stack trace of its thread is written to Tomcat log with a WARN level.

You can specify the number of seconds you want a request to take, at a maximum (default 10 minutes!), and this valve will kill them off if they exceed that.