bc-java icon indicating copy to clipboard operation
bc-java copied to clipboard

ConcurrentModificationException in ProvSSLSessionContext.removeAllExpiredSessions()

Open aberenguel opened this issue 3 years ago • 4 comments

The scenario is a TLS server with some expired sessions. In such instant, the executed code is

SSLContext sslContext = ... // some pre-existing sslContext backed by BC and with some expired sessions
SSLSessionContext sslSessionContext = sslContext.getServerSessionContext();
Enumeration<byte[]> sessionIdsEnumeration = sslSessionContext.getIds(); // <== throws ConcurrentModificationException

The stacktrace:

java.util.ConcurrentModificationException
	at java.util.LinkedHashMap$LinkedHashIterator.remove(LinkedHashMap.java:732)
	at org.bouncycastle.jsse.provider.ProvSSLSessionContext.removeAllExpiredSessions(ProvSSLSessionContext.java:281)
	at org.bouncycastle.jsse.provider.ProvSSLSessionContext.getIds(ProvSSLSessionContext.java:132)

After some debug inspections, I saw the problem is in ProvSSLSessionContext.removeAllExpiredSessions(), more specifically in ProvSSLSessionContext.invalidateIfCreatedBefore(SessionEntry, long). In order to visualize the bug, let's see the code with added comments:

    private void removeAllExpiredSessions()
    {
        processQueue();

        long creationTimeLimit = getCreationTimeLimit(System.currentTimeMillis());

        Iterator<SessionEntry> iter = sessionsByID.values().iterator();
        while (iter.hasNext())
        {
            SessionEntry sessionEntry = iter.next();
            if (invalidateIfCreatedBefore(sessionEntry, creationTimeLimit)) // <== modifies the attribute sessionsByID (see below)
            {
                iter.remove(); // <== tries to modify the attribute this.sessionsByID, which already has been modified
                removeSessionByPeer(sessionEntry);
            }
        }
    }

    private boolean invalidateIfCreatedBefore(SessionEntry sessionEntry, long creationTimeLimit)
    {
        ProvSSLSession session = sessionEntry.get();
        if (session == null)
        {
            return true;
        }
        if (session.getCreationTime() < creationTimeLimit)
        {
            session.invalidate(); // <== here, the attribute sessionsByID is modified by invoking removeSession(byte[] sessionID)
        }
        return !session.isValid();
    }

My solution is to change the removeAllExpiredSessions() method so that the sessionsByID can be changed in the loop.

    private void removeAllExpiredSessions()
    {
        processQueue();

        long creationTimeLimit = getCreationTimeLimit(System.currentTimeMillis());

        Iterator<Entry<SessionID, SessionEntry>> iter = new ArrayList<>(sessionsByID.entrySet()).iterator();
        while (iter.hasNext())
        {
            Entry<SessionID, SessionEntry> entry = iter.next();
            if (invalidateIfCreatedBefore(entry.getValue(), creationTimeLimit))
            {
                sessionsByID.remove(entry.getKey());
                removeSessionByPeer(entry.getValue());
            }
        }
    }

aberenguel avatar Jun 08 '21 02:06 aberenguel

I'm facing the same issue.

ahmcosta avatar Jun 08 '21 11:06 ahmcosta

Thanks for the report. This is now fixed in our git repo and will appear here soon, plus we should get a new beta up shortly.

I preferred a fix that avoids the ProvSSLSessionBase.invalidate call to removeSession instead of copying the entrySet.

peterdettman avatar Jun 09 '21 04:06 peterdettman

Should be fixed by https://github.com/bcgit/bc-java/commit/8dda7ca6a6de19d9c42d72cb4aa73e5af0e87bdd .

A new beta with the fix (1.70b01) is available here: https://downloads.bouncycastle.org/betas/ . Confirmation would be appreciated.

peterdettman avatar Jun 09 '21 08:06 peterdettman

@aberenguel Were you able to verify this was fixed by 1.70 release?

peterdettman avatar Feb 01 '22 13:02 peterdettman