docs: private long doUnwrap(ByteBuffer[] userBuffers, int off, int len) throws IOException {
          if(anyAreSet(state, FLAG_CLOSED)) {
              throw new ClosedChannelException();
          }
          if(outstandingTasks > 0) {
              return 0;
          }
          if(anyAreSet(state, FLAG_READ_REQUIRES_WRITE)) {
              doWrap(null, 0, 0);
              if(allAreClear(state, FLAG_WRITE_REQUIRES_READ)) { //unless a wrap is immediately required we just return
                  return 0;
              }
          }
          boolean bytesProduced = false;
          PooledByteBuffer unwrappedData = this.unwrappedData;
          //copy any exiting data
          if(unwrappedData != null) {
              if(userBuffers != null) {
                  long copied = Buffers.copy(userBuffers, off, len, unwrappedData.getBuffer());
                  if (!unwrappedData.getBuffer().hasRemaining()) {
                      unwrappedData.close();
                      this.unwrappedData = null;
                  }
                  if(copied > 0) {
                      readListenerInvocationCount = 0;
                  }
                  return copied;
              }
          }
          try {
              //we need to store how much data is in the unwrap buffer. If no progress can be made then we unset
              //the data to unwrap flag
              int dataToUnwrapLength;
              //try and read some data if we don't already have some
              if (allAreClear(state, FLAG_DATA_TO_UNWRAP)) {
                  if (dataToUnwrap == null) {
                      dataToUnwrap = bufferPool.allocate();
                  }
                  int res;
                  try {
                      res = source.read(dataToUnwrap.getBuffer());
                  } catch (IOException | RuntimeException | Error e) {
                      dataToUnwrap.close();
                      dataToUnwrap = null;
                      throw e;
                  }
                  dataToUnwrap.getBuffer().flip();
                  if (res == -1) {
                      dataToUnwrap.close();
                      dataToUnwrap = null;
                      notifyReadClosed();
                      return -1;
                  } else if (res == 0 && engine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED) {
                      //its possible there was some data in the buffer from a previous unwrap that had a buffer underflow
                      //if not we just close the buffer so it does not hang around
                      if(!dataToUnwrap.getBuffer().hasRemaining()) {
                          dataToUnwrap.close();
                          dataToUnwrap = null;
                      }
                      return 0;
                  }
              }
              dataToUnwrapLength = dataToUnwrap.getBuffer().remaining();
  
              long original = 0;
              if (userBuffers != null) {
                  original = Buffers.remaining(userBuffers);
              }
              //perform the actual unwrap operation
              //if possible this is done into the the user buffers, however
              //if none are supplied or this results in a buffer overflow then we allocate our own
              SSLEngineResult result;
              boolean unwrapBufferUsed = false;
              try {
                  if (userBuffers != null) {
                      result = engine.unwrap(this.dataToUnwrap.getBuffer(), userBuffers, off, len);
                      if (result.getStatus() == SSLEngineResult.Status.BUFFER_OVERFLOW) {
                          //not enough space in the user buffers
                          //we use our own
                          unwrappedData = bufferPool.allocate();
                          ByteBuffer[] d = new ByteBuffer[len + 1];
                          System.arraycopy(userBuffers, off, d, 0, len);
                          d[len] = unwrappedData.getBuffer();
                          result = engine.unwrap(this.dataToUnwrap.getBuffer(), d);
                          unwrapBufferUsed = true;
                      }
                      bytesProduced = result.bytesProduced() > 0;
                  } else {
                      unwrapBufferUsed = true;
                      if (unwrappedData == null) {
                          unwrappedData = bufferPool.allocate();
                      } else {
                          unwrappedData.getBuffer().compact();
                      }
                      result = engine.unwrap(this.dataToUnwrap.getBuffer(), unwrappedData.getBuffer());
                      bytesProduced = result.bytesProduced() > 0;
                  }
              } finally {
                  if (unwrapBufferUsed) {
                      unwrappedData.getBuffer().flip();
                      if (!unwrappedData.getBuffer().hasRemaining()) {
                          unwrappedData.close();
                          unwrappedData = null;
                      }
                  }
                  this.unwrappedData = unwrappedData;
              }
  
              if (result.getStatus() == SSLEngineResult.Status.CLOSED) {
                  if(dataToUnwrap != null) {
                      dataToUnwrap.close();
                      dataToUnwrap = null;
                  }
                  notifyReadClosed();
                  return -1;
              }
              if (!handleHandshakeResult(result)) {
                  if (this.dataToUnwrap.getBuffer().hasRemaining()
                          && result.getStatus() != SSLEngineResult.Status.BUFFER_UNDERFLOW
                          && dataToUnwrap.getBuffer().remaining() != dataToUnwrapLength) {
                      state |= FLAG_DATA_TO_UNWRAP;
                  } else {
                      state &= ~FLAG_DATA_TO_UNWRAP;
                  }
                  return 0;
              }
              if (result.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) {
                  state &= ~FLAG_DATA_TO_UNWRAP;
              } else if (result.getStatus() == SSLEngineResult.Status.BUFFER_OVERFLOW) {
                  UndertowLogger.REQUEST_LOGGER.sslBufferOverflow(this);
                  IoUtils.safeClose(delegate);
              } else if (this.dataToUnwrap.getBuffer().hasRemaining() && dataToUnwrap.getBuffer().remaining() != dataToUnwrapLength) {
                  state |= FLAG_DATA_TO_UNWRAP;
              } else {
                  state &= ~FLAG_DATA_TO_UNWRAP;
              }
              if (userBuffers == null) {
                  return 0;
              } else {
                  long res = original - Buffers.remaining(userBuffers);
                  if(res > 0) {
                      //if data has been successfully returned this is not a read loop
                      readListenerInvocationCount = 0;
                  }
                  return res;
              }
          } catch (SSLException e) {
              try {
                  try {
                      //we make an effort to write out the final record
                      //this is best effort, there are no guarantees
                      clearWriteRequiresRead();
                      doWrap(null, 0, 0);
                      flush();
                  } catch (Exception e2) {
                      UndertowLogger.REQUEST_LOGGER.debug("Failed to write out final SSL record", e2);
                  }
                  close();
              } catch (Throwable ex) {
                  //we ignore this
                  UndertowLogger.REQUEST_LOGGER.debug("Exception closing SSLConduit after exception in doUnwrap", ex);
              }
              throw e;
          } catch (RuntimeException|IOException|Error e) {
              try {
                  close();
              } catch (Throwable ex) {
                  //we ignore this
                  UndertowLogger.REQUEST_LOGGER.debug("Exception closing SSLConduit after exception in doUnwrap", ex);
              }
              throw e;
          } finally {
              boolean requiresListenerInvocation = false; //if there is data in the buffer and reads are resumed we should re-run the listener
              //we always need to re-invoke if bytes have been produced, as the engine may have buffered some data
              if (bytesProduced || (unwrappedData != null && unwrappedData.isOpen() && unwrappedData.getBuffer().hasRemaining())) {
                  requiresListenerInvocation = true;
              }
              if (dataToUnwrap != null) {
                  //if there is no data in the buffer we just free it
                  if (!dataToUnwrap.getBuffer().hasRemaining()) {
                      dataToUnwrap.close();
                      dataToUnwrap = null;
                      state &= ~FLAG_DATA_TO_UNWRAP;
                  } else if (allAreClear(state, FLAG_DATA_TO_UNWRAP)) {
                      //if there is not enough data in the buffer we compact it to make room for more
                      dataToUnwrap.getBuffer().compact();
                  } else {
                      //there is more data, make sure we trigger a read listener invocation
                      requiresListenerInvocation = true;
                  }
              }
              //if we are in the read listener handshake we don't need to invoke
              //as it is about to be invoked anyway
              if (requiresListenerInvocation && (anyAreSet(state, FLAG_READS_RESUMED) || allAreSet(state, FLAG_WRITE_REQUIRES_READ | FLAG_WRITES_RESUMED)) && !invokingReadListenerHandshake) {
                  runReadListener(false);
              }
          }
      }