https://bz.apache.org/bugzilla/show_bug.cgi?id=70110

            Bug ID: 70110
           Summary: Memory leak: UpgradeInfo objects not removed from
                    UpgradeGroupInfo when WebSocket connection cleanup
                    bypasses normal close path
           Product: Tomcat 9
           Version: 9.0.x
          Hardware: Macintosh
                OS: Mac OS X 10.1
            Status: NEW
          Severity: normal
          Priority: P2
         Component: WebSocket
          Assignee: [email protected]
          Reporter: [email protected]
  Target Milestone: -----

Created attachment 40188
  --> https://bz.apache.org/bugzilla/attachment.cgi?id=40188&action=edit
JUnit test that reproduces the leak by triggering RuntimeException in onOpen()
and verifying UpgradeInfo objects accumulate in UpgradeGroupInfo.upgradeInfos.

**Affects:** Tomcat 9.0.x, 10.x, 11.x (since commit 41497b9883 - October 2020)

  **Summary:**
  UpgradeInfo objects are never removed from UpgradeGroupInfo.upgradeInfos when
an upgraded
  (WebSocket) connection is cleaned up via any path other than the normal close
path in
  ConnectionHandler.process(). On long-running instances with heavy WebSocket
traffic, this
  causes a permanent memory leak — one UpgradeInfo (~80-100 bytes) per affected
connection.

  **Root Cause:**
  There are two code paths in AbstractProtocol.ConnectionHandler that call
release(processor)
  without first calling httpUpgradeHandler.destroy():

  1. **Exception path (lines ~1073-1108):** When processor.process() or
     httpUpgradeHandler.init() throws an exception, the catch blocks fall
through to
     release(processor) without calling destroy(). The normal close path
correctly calls
     destroy() before release(), but the exception path does not.

  2. **release(SocketWrapperBase) (lines ~1173-1176):** When
socketWrapper.close() is called
     directly by the NIO poller (e.g., on thread pool rejection,
CancelledKeyException, or
     endpoint shutdown), it only calls release(processor) — never destroy().

  The cleanup chain that removes UpgradeInfo from tracking:
    httpUpgradeHandler.destroy()
      → connection.close()
        → upgradeInfo.setGroupInfo(null)
          → UpgradeGroupInfo.removeUpgradeInfo()

  This chain is only triggered via destroy(), which is missing in the above
paths.

  **Reproduction:**
  1. Create a WebSocket endpoint where onOpen() throws a RuntimeException
  2. Connect to the endpoint multiple times
  3. Observe that UpgradeGroupInfo.upgradeInfos grows with each connection
  4. The UpgradeInfo objects are never removed

-- 
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to