-
Bug
-
Resolution: Done
-
Major
-
None
-
None
It looks like there is a potential race in the io.undertow.servlet.spec.HttpServletRequestImpl. The asyncStarted is set to true, then the AsyncContextImpl is created. I'm seeing this failure occasionally with a RESTEasy test. I'm not able to duplicate it locally, but it happens periodically on CI. Below is an example stack trace:
07:49:50,752 ERROR [org.jboss.resteasy.core.providerfactory.DefaultExceptionMapper] (XNIO-1 task-3) RESTEASY002375: Error processing request POST /test/multi-injected - org.jboss.resteasy.plugins.providers.multipart.MultipartEntityPartProviderTest$TestResource.multipleInjectable: java.lang.NullPointerException: Cannot invoke "io.undertow.servlet.spec.AsyncContextImpl.addAsyncTask(java.lang.Runnable)" because "this.asyncContext" is null at io.undertow.servlet.spec.ServletOutputStreamImpl.setWriteListener(ServletOutputStreamImpl.java:807) at org.jboss.resteasy.plugins.server.servlet.HttpServletResponseWrapper$WrappedServletOutputStream.setWriteListener(HttpServletResponseWrapper.java:591) at org.jboss.resteasy.plugins.server.servlet.HttpServletResponseWrapper$DeferredOutputStream.queue(HttpServletResponseWrapper.java:298) at org.jboss.resteasy.plugins.server.servlet.HttpServletResponseWrapper$DeferredOutputStream.asyncWrite(HttpServletResponseWrapper.java:281) at org.jboss.resteasy.util.CommitHeaderAsyncOutputStream.asyncWrite(CommitHeaderAsyncOutputStream.java:80) at org.jboss.resteasy.spi.AsyncOutputStream.asyncWrite(AsyncOutputStream.java:25) at org.jboss.resteasy.plugins.providers.multipart.AbstractMultipartWriter.asyncWritePart(AbstractMultipartWriter.java:202) at org.jboss.resteasy.plugins.providers.multipart.AbstractMultipartFormDataWriter.lambda$asyncWriteParts$0(AbstractMultipartFormDataWriter.java:62) at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187) at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2341) at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:144) at org.jboss.resteasy.plugins.providers.multipart.AbstractMultipartFormDataWriter.asyncWriteParts(AbstractMultipartFormDataWriter.java:62) at org.jboss.resteasy.plugins.providers.multipart.AbstractMultipartWriter.asyncWrite(AbstractMultipartWriter.java:137) at org.jboss.resteasy.plugins.providers.multipart.MultipartEntityPartWriter.lambda$asyncWriteTo$1(MultipartEntityPartWriter.java:58) at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1150) at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1773) at org.jboss.resteasy.concurrent.ContextualExecutors.lambda$runnable$2(ContextualExecutors.java:312) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) at java.base/java.lang.Thread.run(Thread.java:1583)
The ServletOutputStreamImpl.setWriterListener() looks like the following:
asyncContext = (AsyncContextImpl) servletRequest.getAsyncContext(); listener = writeListener; //we register the write listener on the underlying connection //so we don't have to force the creation of the response channel //under normal circumstances this will break write listener delegation this.internalListener = new WriteChannelListener(); if (this.channel != null) { this.channel.getWriteSetter().set(internalListener); } //we resume from an async task, after the request has been dispatched asyncContext.addAsyncTask(new Runnable() { @Override public void run() { asyncIoStarted = true; if (channel == null) { servletRequestContext.getExchange().getIoThread().execute(new Runnable() { @Override public void run() { internalListener.handleEvent(null); } }); } else { channel.resumeWrites(); } } });
The servletRequest.getAsyncContext() should throw an IllegalStateException if startAsync() was not invoked, so that must have been done. However, the returned asyncContext ends up being null.
I'm not really sure what the best non-blocking/non-locking fix is. The first thought was to use a try/finally where we set the asyncStarted in the finally in the io.undertow.servlet.spec.HttpServletRequestImpl. However, if an error occurs creating the AsyncContextImpl, then we leave the request in an invalid state. That still leaves a potential for the async context to get started twice as well.
- is incorporated by
-
WFCORE-6695 CVE-2023-4639 Upgrade Undertow to 2.3.11.Final
- Resolved
- links to