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.
Summary
AtomValenceTool.getValence()in CDK 2.13 can throw aNullPointerExceptionwhen 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
valencesTableis assigned a new emptyHashMapbefore itsentries are added. Another thread can therefore observe a non-null but
incomplete map and receive
nullfromMap.get(). Automatic unboxing ofthat value to
intthen throwsNullPointerException.Relevant source:
base/standard/src/main/java/org/openscience/cdk/qsar/AtomValenceTool.javaIn tag
cdk-2.13, the relevant code is around lines 36–91.Environment
5a42221d412dca53e48fdfc0bfae645310f9de80082800af801d06a87df32411Minimal reproducer
Compile and run:
On Windows, replace
:with;in the runtime class path.Actual behavior
The failure reproduced on the first run:
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:
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:
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.