From aaf98c116308f1ae32cc7efb3a3028bb40ba5746 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Wed, 8 Jul 2026 15:02:52 +0300 Subject: [PATCH] Make the shared Key.SDF date formatter thread-safe (CodeQL java/thread-unsafe-dateformat) Key.SDF was a public static SimpleDateFormat shared across all threads. SimpleDateFormat is not thread-safe: concurrent format()/parse() calls corrupt its internal Calendar state, producing garbled output, wrong dates, or ArrayIndexOutOfBoundsException. Key.SDF is reachable from Key.decodeDisplayable/toString, Value.toString, KeyParser and the Swing ValueInspectorTreeNode, all of which can run on multiple threads. Wrap the formatter in a ThreadLocal so each thread gets its own SimpleDateFormat instance. The pattern ("yyyyMMddHHmmss.SSSZ") and thus the formatting/parsing semantics are unchanged; only the shared mutable state is removed. Update the four call sites to Key.SDF.get(). The remaining private static SimpleDateFormat fields (JournalTool, VolumeHeader, CheckpointManager, and the AbstractSuite test) were not flagged by CodeQL and are left unchanged. --- persistit/core/src/main/java/com/persistit/Key.java | 12 +++++++++--- .../core/src/main/java/com/persistit/KeyParser.java | 3 ++- .../core/src/main/java/com/persistit/Value.java | 3 ++- .../com/persistit/ui/ValueInspectorTreeNode.java | 3 ++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/persistit/core/src/main/java/com/persistit/Key.java b/persistit/core/src/main/java/com/persistit/Key.java index 219c032754..440f627ab2 100644 --- a/persistit/core/src/main/java/com/persistit/Key.java +++ b/persistit/core/src/main/java/com/persistit/Key.java @@ -1,5 +1,6 @@ /** * Copyright 2005-2012 Akiban Technologies, Inc. + * Portions Copyrighted 2026 3A Systems, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -504,14 +505,19 @@ public enum Direction { public final static EdgeValue AFTER = new EdgeValue(true); /** - * The java.text.SimpleDateFormat used in formatting + * A thread-local java.text.SimpleDateFormat used in formatting * Date- valued keys in the {@link #decodeDisplayable} methods. * This format also governs the conversion of dates for the * toString method. This format provides millisecond resolution * so that the precise stored representation of a date used as a key can be * represented exactly, but readably, in the displayable version. + *

+ * SimpleDateFormat is not thread-safe, so the instance is held + * in a {@link ThreadLocal}; call {@link ThreadLocal#get()} to obtain the + * per-thread formatter, e.g. SDF.get().format(date). */ - public final static SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMddHHmmss.SSSZ"); + public final static ThreadLocal SDF = ThreadLocal + .withInitial(() -> new SimpleDateFormat("yyyyMMddHHmmss.SSSZ")); /** * Displayable prefix for boolean values (optional for input, implied and @@ -3139,7 +3145,7 @@ else if (cl == Boolean.class) { else if (cl == Date.class) { Util.append(sb, PREFIX_DATE); - Util.append(sb, SDF.format(decodeDate())); + Util.append(sb, SDF.get().format(decodeDate())); } else if (cl == byte[].class) { diff --git a/persistit/core/src/main/java/com/persistit/KeyParser.java b/persistit/core/src/main/java/com/persistit/KeyParser.java index 3fc768a06d..13c6561be4 100644 --- a/persistit/core/src/main/java/com/persistit/KeyParser.java +++ b/persistit/core/src/main/java/com/persistit/KeyParser.java @@ -1,5 +1,6 @@ /** * Copyright 2005-2012 Akiban Technologies, Inc. + * Portions Copyrighted 2026 3A Systems, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -293,7 +294,7 @@ else if (matchExactString("false")) } } else if (matchExactString(Key.PREFIX_DATE) || matchExactString(Key.PREFIX_DATE0)) { if (matchNumber(true, true)) { - key.append(Key.SDF.parse(_sb.toString())); + key.append(Key.SDF.get().parse(_sb.toString())); result = true; } } else if (matchExactString(Key.PREFIX_BYTE_ARRAY)) { diff --git a/persistit/core/src/main/java/com/persistit/Value.java b/persistit/core/src/main/java/com/persistit/Value.java index 7e7cdb1729..5a5d05d2a6 100644 --- a/persistit/core/src/main/java/com/persistit/Value.java +++ b/persistit/core/src/main/java/com/persistit/Value.java @@ -1,5 +1,6 @@ /** * Copyright 2005-2012 Akiban Technologies, Inc. + * Portions Copyrighted 2026 3A Systems, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1566,7 +1567,7 @@ private void appendDisplayable(final StringBuilder sb, final Object value, final sb.append("..."); } else if (cl == Date.class) { appendParenthesizedFriendlyClassName(sb, cl); - sb.append(Key.SDF.format((Date) value)); + sb.append(Key.SDF.get().format((Date) value)); } else if (value instanceof Number) { sb.append('('); sb.append(className.startsWith("java.lang.") ? className.substring(10) : className); diff --git a/persistit/ui/src/main/java/com/persistit/ui/ValueInspectorTreeNode.java b/persistit/ui/src/main/java/com/persistit/ui/ValueInspectorTreeNode.java index 3eb835090b..906f9d41ab 100644 --- a/persistit/ui/src/main/java/com/persistit/ui/ValueInspectorTreeNode.java +++ b/persistit/ui/src/main/java/com/persistit/ui/ValueInspectorTreeNode.java @@ -1,5 +1,6 @@ /** * Copyright 2005-2012 Akiban Technologies, Inc. + * Portions Copyrighted 2026 3A Systems, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -156,7 +157,7 @@ String displayable(final String fieldName, final Class type, final Object object sb.append('\"'); } else if (object instanceof Date) { sb.append(" = "); - sb.append(Key.SDF.format((Date) object)); + sb.append(Key.SDF.get().format((Date) object)); } else if (type == Boolean.class || type == Byte.class || type == Short.class || type == Character.class || type == Integer.class || type == Long.class || type == Float.class || type == Double.class || type == BigInteger.class || type == BigDecimal.class) {