Skip to content

AtomValenceTool is not thread-safe: concurrent first use can observe a partially initialized valence table #1308

Description

@kmansouri

Summary

AtomValenceTool.getValence() in CDK 2.13 can throw a
NullPointerException when it is called concurrently for the first time.

I found this while porting PaDEL-Descriptor to CDK 2.13, but the reproducer
below uses only CDK classes and does not depend on PaDEL.

The static valencesTable is assigned a new empty HashMap before its
entries are added. Another thread can therefore observe a non-null but
incomplete map and receive null from Map.get(). Automatic unboxing of
that value to int then throws NullPointerException.

Relevant source:

base/standard/src/main/java/org/openscience/cdk/qsar/AtomValenceTool.java

In tag cdk-2.13, the relevant code is around lines 36–91.

Environment

  • CDK: 2.13 official all-dependencies JAR
  • JAR SHA-256:
    5a42221d412dca53e48fdfc0bfae645310f9de80082800af801d06a87df32411
  • Java: OpenJDK 21.0.11
  • OS: Debian GNU/Linux 13, x86_64

Minimal reproducer

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

import org.openscience.cdk.Atom;
import org.openscience.cdk.qsar.AtomValenceTool;

public final class AtomValenceRace {
    public static void main(String[] args) throws Exception {
        int n = 256;

        CountDownLatch ready = new CountDownLatch(n);
        CountDownLatch start = new CountDownLatch(1);
        AtomicReference<Throwable> failure = new AtomicReference<>();
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < n; i++) {
            Thread thread = new Thread(() -> {
                ready.countDown();

                try {
                    start.await();

                    // "Co" is the last element inserted into the current table,
                    // which makes the partial-initialization window easy to see.
                    AtomValenceTool.getValence(new Atom("Co"));
                } catch (Throwable ex) {
                    failure.compareAndSet(null, ex);
                }
            });

            threads.add(thread);
            thread.start();
        }

        ready.await();
        start.countDown();

        for (Thread thread : threads) {
            thread.join();
        }

        if (failure.get() != null) {
            throw new AssertionError(failure.get());
        }
    }
}

Compile and run:

javac -proc:none -cp cdk-2.13.jar AtomValenceRace.java
java -cp cdk-2.13.jar:. AtomValenceRace

On Windows, replace : with ; in the runtime class path.

Actual behavior

The failure reproduced on the first run:

java.lang.NullPointerException:
Cannot invoke "java.lang.Integer.intValue()"
because the return value of "java.util.Map.get(Object)" is null

    at org.openscience.cdk.qsar.AtomValenceTool.getValence(
        AtomValenceTool.java:91)

Expected behavior

Concurrent calls for supported element symbols should return their valence
without exceptions, including during the first use of the class.

Apparent cause

The current initialization follows this pattern:

if (valencesTable == null) {
    valencesTable = new HashMap<>();
    valencesTable.put(...);
    // remaining entries
}

The static field is published before initialization has completed, with no
synchronization or safe-publication mechanism.

Suggested fix

Initialize the complete table during class initialization and publish it only
after it has been fully populated, for example:

private static final Map<String, Integer> VALENCES = createValences();

private static Map<String, Integer> createValences() {
    Map<String, Integer> values = new HashMap<>();
    values.put("H", 1);
    // remaining entries
    return Collections.unmodifiableMap(values);
}

It may also be useful to handle unsupported element symbols explicitly instead
of relying on unboxing a possibly null Integer.

A regression test could coordinate concurrent first calls with
CountDownLatch.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions