Skip to content

Thread-unsafe usage of HttpServletResponse corrupting jetty HttpFields state in reactive ResponseBodyEmitter handlers #9175

@SpComb

Description

@SpComb

Affects: 5.2.5.RELEASE


We have a Spring Web MVC + jetty application using SSE for notifications. We recently converted our SSE controller from returning a SseEmitter to returning a Flux<ServerSentEvent<?>>, whereupon we started seeing sporadic NullPointerExceptions from jetty Response.setHeader calls for something on the order of 0.02% of requests for these SSE endpoints:

2020-05-12 11:15:53.837|WARN|1|qtp453671855-27|||org.eclipse.jetty.server.HttpChannel|MAIN||||/...
java.lang.NullPointerException: null
	at org.eclipse.jetty.http.HttpFields.put(HttpFields.java:630) ~[jetty-http-9.4.20.v20190813.jar!/:9.4.20.v20190813]
	at org.eclipse.jetty.http.HttpFields.put(HttpFields.java:658) ~[jetty-http-9.4.20.v20190813.jar!/:9.4.20.v20190813]
	at org.eclipse.jetty.server.Response.setHeader(Response.java:582) ~[jetty-server-9.4.20.v20190813.jar!/:9.4.20.v20190813]
	at javax.servlet.http.HttpServletResponseWrapper.setHeader(HttpServletResponseWrapper.java:203) ~[javax.servlet-api-3.1.0.jar!/:3.1.0]
	at org.springframework.security.web.firewall.FirewalledResponse.setHeader(FirewalledResponse.java:49) ~[spring-security-web-5.2.2.RELEASE.jar!/:5.2.2.RELEASE]
	at org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.writeHeaders(XFrameOptionsHeaderWriter.java:99) ~[spring-security-web-5.2.2.RELEASE.jar!/:5.2.2.RELEASE]
   ....

Looking at the jetty HttpFields implementation, these NPEs seem to be caused by a corrupted internal _fields state containing nulls.

With jetty 9.4.20 these exceptions cause the connection to be aborted, but with newer version of jetty (at least 9.4.27) these corrupted HttpFields put the jetty HttpChannel into a busyloop that crashes the entire server: jetty/jetty.project#4860

Investigating the underlying NPE issue together with the jetty maintainers we were unable to find any direct calls to the jetty APIs that would introduce null values in the HttpFields state, but looking at the implementation of the jetty HttpFields structure used in Response.setHeaders/addHeaders etc, it is suspectible to internal state corruption if called concurrently from multiple threads, and is documented as non-threadsafe:

* <p>This class is not synchronized as it is expected that modifications will only be performed by a
* single thread.

This kind of corruption caused by concurrent multi-threaded accesses also matches up with our observation of very rarely occuring state corruption/crashes. And indeed, using jdb to trace calls to these header-related methods for a single HTTP request, I can see calls from multiple different threads!

Some of the calls are from the jetty server thread, via the servlet filter chain, which is the correct usage:

  [1] org.eclipse.jetty.server.Response.addHeader (Response.java:627)
  [2] javax.servlet.http.HttpServletResponseWrapper.addHeader (HttpServletResponseWrapper.java:212)
  [3] org.springframework.security.web.firewall.FirewalledResponse.addHeader (FirewalledResponse.java:55)
  [4] javax.servlet.http.HttpServletResponseWrapper.addHeader (HttpServletResponseWrapper.java:212)
  [5] org.springframework.security.web.util.OnCommittedResponseWrapper.addHeader (OnCommittedResponseWrapper.java:63)
  [6] org.springframework.web.cors.DefaultCorsProcessor.processRequest (DefaultCorsProcessor.java:63)
  [7] org.springframework.web.servlet.handler.AbstractHandlerMapping$CorsInterceptor.preHandle (AbstractHandlerMapping.java:582)
  [8] org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle (HandlerExecutionChain.java:141)
  [9] org.springframework.web.servlet.DispatcherServlet.doDispatch (DispatcherServlet.java:1,035)
  [10] org.springframework.web.servlet.DispatcherServlet.doService (DispatcherServlet.java:943)
  [11] org.springframework.web.servlet.FrameworkServlet.processRequest (FrameworkServlet.java:1,006)
  [12] org.springframework.web.servlet.FrameworkServlet.doGet (FrameworkServlet.java:898)
  [13] javax.servlet.http.HttpServlet.service (HttpServlet.java:687)
  [14] org.springframework.web.servlet.FrameworkServlet.service (FrameworkServlet.java:883)
  [15] javax.servlet.http.HttpServlet.service (HttpServlet.java:790)
  [16] org.eclipse.jetty.servlet.ServletHolder.handle (ServletHolder.java:755)
  [17] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,617)
  [18] com.example.common.audit.MDCLogFilter.doFilter (MDCLogFilter.java:67)
  [19] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
  [20] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:320)
  [21] org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke (FilterSecurityInterceptor.java:126)
  [22] org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter (FilterSecurityInterceptor.java:90)
  [23] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [24] org.springframework.security.web.access.ExceptionTranslationFilter.doFilter (ExceptionTranslationFilter.java:118)
  [25] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [26] org.springframework.security.web.session.SessionManagementFilter.doFilter (SessionManagementFilter.java:137)
  [27] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [28] org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter (AnonymousAuthenticationFilter.java:111)
  [29] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [30] org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter (SecurityContextHolderAwareRequestFilter.java:158)
  [31] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [32] com.example.common.auth.WhitelistIpAuthenticationFilter.doFilter (WhitelistIpAuthenticationFilter.java:37)
  [33] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [34] org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal (BasicAuthenticationFilter.java:204)
  [35] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
  [36] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [37] com.example.common.auth.StatelessAuthenticationFilter.doFilter (StatelessAuthenticationFilter.java:49)
  [38] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [39] org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter (HeaderWriterFilter.java:92)
  [40] org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal (HeaderWriterFilter.java:77)
  [41] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
  [42] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [43] org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter (SecurityContextPersistenceFilter.java:105)
  [44] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
  [45] org.springframework.security.web.FilterChainProxy.doFilterInternal (FilterChainProxy.java:215)
  [46] org.springframework.security.web.FilterChainProxy.doFilter (FilterChainProxy.java:178)
  [47] org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate (DelegatingFilterProxy.java:358)
  [48] org.springframework.web.filter.DelegatingFilterProxy.doFilter (DelegatingFilterProxy.java:271)
  [49] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
  [50] org.springframework.web.filter.RequestContextFilter.doFilterInternal (RequestContextFilter.java:100)
  [51] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
  [52] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
  [53] org.springframework.web.filter.FormContentFilter.doFilterInternal (FormContentFilter.java:93)
  [54] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
  [55] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
  [56] org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal (WebMvcMetricsFilter.java:109)
  [57] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
  [58] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
  [59] org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal (CharacterEncodingFilter.java:201)
  [60] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
  [61] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
  [62] org.eclipse.jetty.servlet.ServletHandler.doHandle (ServletHandler.java:545)
  [63] org.eclipse.jetty.server.handler.ScopedHandler.handle (ScopedHandler.java:143)
  [64] org.eclipse.jetty.security.SecurityHandler.handle (SecurityHandler.java:590)
  [65] org.eclipse.jetty.server.handler.HandlerWrapper.handle (HandlerWrapper.java:127)
  [66] org.eclipse.jetty.server.handler.ScopedHandler.nextHandle (ScopedHandler.java:235)
  [67] org.eclipse.jetty.server.session.SessionHandler.doHandle (SessionHandler.java:1,610)
  [68] org.eclipse.jetty.server.handler.ScopedHandler.nextHandle (ScopedHandler.java:233)
  [69] org.eclipse.jetty.server.handler.ContextHandler.doHandle (ContextHandler.java:1,300)
  [70] org.eclipse.jetty.server.handler.ScopedHandler.nextScope (ScopedHandler.java:188)
  [71] org.eclipse.jetty.servlet.ServletHandler.doScope (ServletHandler.java:485)
  [72] org.eclipse.jetty.server.session.SessionHandler.doScope (SessionHandler.java:1,580)
  [73] org.eclipse.jetty.server.handler.ScopedHandler.nextScope (ScopedHandler.java:186)
  [74] org.eclipse.jetty.server.handler.ContextHandler.doScope (ContextHandler.java:1,215)
  [75] org.eclipse.jetty.server.handler.ScopedHandler.handle (ScopedHandler.java:141)
  [76] org.eclipse.jetty.server.handler.StatisticsHandler.handle (StatisticsHandler.java:173)
  [77] org.eclipse.jetty.server.handler.HandlerWrapper.handle (HandlerWrapper.java:127)
  [78] org.eclipse.jetty.server.Server.handle (Server.java:500)
  [79] org.eclipse.jetty.server.HttpChannel.lambda$handle$1 (HttpChannel.java:383)
  [80] org.eclipse.jetty.server.HttpChannel$$Lambda$1167.1412565883.dispatch (null)
  [81] org.eclipse.jetty.server.HttpChannel.dispatch (HttpChannel.java:547)
  [82] org.eclipse.jetty.server.HttpChannel.handle (HttpChannel.java:375)
  [83] org.eclipse.jetty.server.HttpConnection.onFillable (HttpConnection.java:273)
  [84] org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded (AbstractConnection.java:311)
  [85] org.eclipse.jetty.io.FillInterest.fillable (FillInterest.java:103)
  [86] org.eclipse.jetty.io.ChannelEndPoint$2.run (ChannelEndPoint.java:117)
  [87] org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask (EatWhatYouKill.java:336)
  [88] org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce (EatWhatYouKill.java:313)
  [89] org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce (EatWhatYouKill.java:171)
  [90] org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produce (EatWhatYouKill.java:135)
  [91] org.eclipse.jetty.io.ManagedSelector$$Lambda$1140.431966204.run (null)
  [92] org.eclipse.jetty.util.thread.QueuedThreadPool.runJob (QueuedThreadPool.java:806)
  [93] org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run (QueuedThreadPool.java:938)
  [94] java.lang.Thread.run (Thread.java:834)

However, there are also calls to addHeader from the ThreadPoolExecutor used in the spring framework ResponseBodyEmitterReturnValueHandler implementation, via the org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler$SseEmitterSubscriber:

  [1] org.eclipse.jetty.server.Response.addHeader (Response.java:627)
  [2] javax.servlet.http.HttpServletResponseWrapper.addHeader (HttpServletResponseWrapper.java:212)
  [3] org.springframework.security.web.firewall.FirewalledResponse.addHeader (FirewalledResponse.java:55)
  [4] javax.servlet.http.HttpServletResponseWrapper.addHeader (HttpServletResponseWrapper.java:212)
  [5] org.springframework.security.web.util.OnCommittedResponseWrapper.addHeader (OnCommittedResponseWrapper.java:63)
  [6] org.springframework.http.server.ServletServerHttpResponse.lambda$writeHeaders$0 (ServletServerHttpResponse.java:104)
  [7] org.springframework.http.server.ServletServerHttpResponse$$Lambda$1241.488706756.accept (null)
  [8] java.util.Map.forEach (Map.java:661)
  [9] org.springframework.http.server.ServletServerHttpResponse.writeHeaders (ServletServerHttpResponse.java:102)
  [10] org.springframework.http.server.ServletServerHttpResponse.getBody (ServletServerHttpResponse.java:83)
  [11] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$StreamingServletServerHttpResponse.getBody (ResponseBodyEmitterReturnValueHandler.java:278)
  [12] org.springframework.http.converter.StringHttpMessageConverter.writeInternal (StringHttpMessageConverter.java:122)
  [13] org.springframework.http.converter.StringHttpMessageConverter.writeInternal (StringHttpMessageConverter.java:44)
  [14] org.springframework.http.converter.AbstractHttpMessageConverter.write (AbstractHttpMessageConverter.java:227)
  [15] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$HttpMessageConvertingHandler.sendInternal (ResponseBodyEmitterReturnValueHandler.java:210)
  [16] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$HttpMessageConvertingHandler.send (ResponseBodyEmitterReturnValueHandler.java:203)
  [17] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.sendInternal (ResponseBodyEmitter.java:189)
  [18] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.send (ResponseBodyEmitter.java:183)
  [19] org.springframework.web.servlet.mvc.method.annotation.SseEmitter.send (SseEmitter.java:126)
  [20] org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler$SseEmitterSubscriber.send (ReactiveTypeHandler.java:365)
  [21] org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler$AbstractEmitterSubscriber.run (ReactiveTypeHandler.java:308)
  [22] java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1,128)
  [23] java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:628)
  [24] java.lang.Thread.run (Thread.java:834)

Looking at the code, the ResponseBodyEmitter created by the ResponseBodyEmitterReturnValueHandler/ReactiveTypeHandler should not be writing any HTTP response headers outside of the extendResponse called in the servlet thread, but the ReactiveTypeHandler subscription's SseEmitter.send call in the TaskExecutor thread is also invoking ServletServerHttpResponse.writeHeaders via the HttpMessageConvertingHandler.sendInternal. EDIT: see comment below on spring-security HeaderWriterFilter behavior instead

Complete error log and jdb traces atttached:

EDIT: edited stack traces to censor custom package names; use the first task-9 thread stack from the jdb traces as the example

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: webAn issue in web modules (web, webmvc)type: bugA general bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions