This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 0c75cd1e3ac85944fb8ecbcf88cc38e9fa1fe4d5
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Fri Feb 21 11:54:55 2025 +0000

    Improve CVE-2024-56337 protection
    
    Don't use reflection unless necessary. This means less impact for those
    using Tomcat in an embedded environment.
---
 bin/catalina.bat                                   |   2 +
 bin/catalina.sh                                    |   2 +
 .../org/apache/tomcat/util/compat/Jre12Compat.java | 171 +++++++++++++++++++++
 .../org/apache/tomcat/util/compat/Jre16Compat.java |   2 +-
 java/org/apache/tomcat/util/compat/JreCompat.java  | 134 ++++++++++++----
 .../tomcat/util/compat/LocalStrings.properties     |   5 +-
 webapps/docs/changelog.xml                         |   6 +-
 7 files changed, 289 insertions(+), 33 deletions(-)

diff --git a/bin/catalina.bat b/bin/catalina.bat
index 9fc3da0369..71e077dac7 100755
--- a/bin/catalina.bat
+++ b/bin/catalina.bat
@@ -245,6 +245,8 @@ set 
LOGGING_MANAGER=-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogMa
 
 rem Configure JAVA 9 specific start-up parameters
 set "JDK_JAVA_OPTIONS=%JDK_JAVA_OPTIONS% 
--add-opens=java.base/java.lang=ALL-UNNAMED"
+set "JDK_JAVA_OPTIONS=%JDK_JAVA_OPTIONS% 
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"
+set "JDK_JAVA_OPTIONS=%JDK_JAVA_OPTIONS% 
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
 set "JDK_JAVA_OPTIONS=%JDK_JAVA_OPTIONS% 
--add-opens=java.base/java.io=ALL-UNNAMED"
 set "JDK_JAVA_OPTIONS=%JDK_JAVA_OPTIONS% 
--add-opens=java.base/java.util=ALL-UNNAMED"
 set "JDK_JAVA_OPTIONS=%JDK_JAVA_OPTIONS% 
--add-opens=java.base/java.util.concurrent=ALL-UNNAMED"
diff --git a/bin/catalina.sh b/bin/catalina.sh
index 8d07001214..e312cf5940 100755
--- a/bin/catalina.sh
+++ b/bin/catalina.sh
@@ -332,6 +332,8 @@ fi
 
 # Add the JAVA 9 specific start-up parameters required by Tomcat
 JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS 
--add-opens=java.base/java.lang=ALL-UNNAMED"
+JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS 
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"
+JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS 
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
 JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS --add-opens=java.base/java.io=ALL-UNNAMED"
 JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS 
--add-opens=java.base/java.util=ALL-UNNAMED"
 JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS 
--add-opens=java.base/java.util.concurrent=ALL-UNNAMED"
diff --git a/java/org/apache/tomcat/util/compat/Jre12Compat.java 
b/java/org/apache/tomcat/util/compat/Jre12Compat.java
new file mode 100644
index 0000000000..fee0a17027
--- /dev/null
+++ b/java/org/apache/tomcat/util/compat/Jre12Compat.java
@@ -0,0 +1,171 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomcat.util.compat;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.res.StringManager;
+
+public class Jre12Compat extends Jre9Compat {
+
+    private static final Log log = LogFactory.getLog(Jre12Compat.class);
+    private static final StringManager sm = 
StringManager.getManager(Jre12Compat.class);
+
+    private static final boolean supported;
+
+    static {
+        // Don't need any Java 12 specific classes (yet) so just test for one 
of
+        // the new ones for now.
+        Class<?> c1 = null;
+        try {
+            c1 = Class.forName("java.text.CompactNumberFormat");
+
+        } catch (ReflectiveOperationException e) {
+            // Must be pre-Java 12
+            log.debug(sm.getString("jre12Compat.javaPre12"), e);
+        }
+
+        supported = (c1 != null);
+    }
+
+    static boolean isSupported() {
+        return supported;
+    }
+
+
+    /*
+     * The behaviour of the canonical file name cache varies by Java version.
+     *
+     * The cache was removed in Java 21 so these methods and the associated 
code can be removed once the minimum Java
+     * version is 21.
+     *
+     * For 12 <= Java <= 20, the cache was present but disabled by default.
+     *
+     * For Java < 12, the cache was enabled by default. Tomcat assumes the 
cache is enabled unless proven otherwise.
+     *
+     * Tomcat 10.1 has a minimum Java version of 11.
+     *
+     * The static field in java.io.FileSystem will be set before any 
application code gets a chance to run. Therefore,
+     * the value of that field can be determined by looking at the command 
line arguments. This enables us to determine
+     * the status without having using reflection.
+     *
+     * This is Java 12 and later.
+     */
+    @Override
+    public boolean isCanonCachesDisabled() {
+        if (canonCachesDisabled != null) {
+            return canonCachesDisabled.booleanValue();
+        }
+        synchronized (canonCachesDisabledLock) {
+            if (canonCachesDisabled != null) {
+                return canonCachesDisabled.booleanValue();
+            }
+
+            List<String> args = 
ManagementFactory.getRuntimeMXBean().getInputArguments();
+            for (String arg : args) {
+                // If any command line argument attempts to enable the cache, 
assume it is enabled.
+                if (arg.startsWith(USE_CANON_CACHES_CMD_ARG)) {
+                    String value = 
arg.substring(USE_CANON_CACHES_CMD_ARG.length());
+                    boolean cacheEnabled = 
Boolean.valueOf(value).booleanValue();
+                    if (cacheEnabled) {
+                        canonCachesDisabled = Boolean.FALSE;
+                        return false;
+                    }
+                }
+            }
+            canonCachesDisabled = Boolean.TRUE;
+            return true;
+        }
+    }
+
+
+    /*
+     * Java 12 increased security around reflection so additional code is 
required to disable the cache since a final
+     * field needs to be changed.
+     */
+    @Override
+    protected void ensureUseCanonCachesFieldIsPopulated() {
+        if (useCanonCachesField != null) {
+            return;
+        }
+        synchronized (useCanonCachesFieldLock) {
+            if (useCanonCachesField != null) {
+                return;
+            }
+
+            Field f = null;
+            try {
+                Class<?> clazz = Class.forName("java.io.FileSystem");
+                f = clazz.getDeclaredField("useCanonCaches");
+                // Need this because the 'useCanonCaches' field is private
+                f.setAccessible(true);
+
+                /*
+                 * Need this in Java 12 to 17 (and it only works up to Java 
17) because the 'useCanonCaches' field is
+                 * final.
+                 *
+                 * This will fail in Java 18 to 20 but since those versions 
are no longer supported it is acceptable for
+                 * the attempt to set the 'useCanonCaches' field to fail. 
Users that really want to use Java 18 to 20
+                 * will have to ensure that they do not explicitly enable the 
canonical file name cache.
+                 */
+                Method privateLookupInMethod = 
MethodHandles.class.getDeclaredMethod("privateLookupIn", Class.class, 
Lookup.class);
+                Method findVarHandleMethod = 
Lookup.class.getDeclaredMethod("findVarHandle", Class.class, String.class, 
Class.class);
+                clazz = Class.forName("java.lang.invoke.VarHandle");
+
+                Lookup lookup = (Lookup) privateLookupInMethod.invoke(null, 
Field.class, MethodHandles.lookup());
+                Object modifiers = findVarHandleMethod.invoke(lookup, 
Field.class, "modifiers", int.class);
+                Method setMethod = null;
+                try {
+                    setMethod = modifiers.getClass().getDeclaredMethod("set", 
modifiers.getClass(), Object.class, int.class);
+                } catch (NoSuchMethodException e) {
+                    /*
+                     * Method signature changed between Java 14 and Java 15. 
This hack avoids creating Jre15Compat for
+                     * this one line.
+                     */
+                    setMethod = modifiers.getClass().getDeclaredMethod("set", 
clazz, Object.class, int.class);
+                }
+                setMethod.setAccessible(true);
+                setMethod.invoke(null, modifiers, f, 
Integer.valueOf(f.getModifiers() & ~Modifier.FINAL));
+            } catch (NoSuchMethodException e) {
+                // Make sure field is not set.
+                f = null;
+                log.warn(sm.getString("jreCompat.useCanonCaches.java18"), e);
+            } catch (Throwable t) {
+                ExceptionUtils.handleThrowable(t);
+                // Make sure field is not set.
+                f = null;
+                log.warn(sm.getString("jreCompat.useCanonCaches.init"), t);
+            }
+
+            if (f == null) {
+                useCanonCachesField = Optional.empty();
+            } else {
+                useCanonCachesField = Optional.of(f);
+            }
+        }
+    }
+}
diff --git a/java/org/apache/tomcat/util/compat/Jre16Compat.java 
b/java/org/apache/tomcat/util/compat/Jre16Compat.java
index e001acc540..70e5fb1094 100644
--- a/java/org/apache/tomcat/util/compat/Jre16Compat.java
+++ b/java/org/apache/tomcat/util/compat/Jre16Compat.java
@@ -28,7 +28,7 @@ import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.util.res.StringManager;
 
-class Jre16Compat extends Jre9Compat {
+class Jre16Compat extends Jre12Compat {
 
     private static final Log log = LogFactory.getLog(Jre16Compat.class);
     private static final StringManager sm = 
StringManager.getManager(Jre16Compat.class);
diff --git a/java/org/apache/tomcat/util/compat/JreCompat.java 
b/java/org/apache/tomcat/util/compat/JreCompat.java
index fac8d62f24..af6cbea7e5 100644
--- a/java/org/apache/tomcat/util/compat/JreCompat.java
+++ b/java/org/apache/tomcat/util/compat/JreCompat.java
@@ -18,6 +18,7 @@ package org.apache.tomcat.util.compat;
 
 import java.io.File;
 import java.io.IOException;
+import java.lang.management.ManagementFactory;
 import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
@@ -29,6 +30,8 @@ import java.nio.channels.ServerSocketChannel;
 import java.nio.channels.SocketChannel;
 import java.security.PrivilegedExceptionAction;
 import java.util.Deque;
+import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CompletionException;
 import java.util.jar.JarFile;
@@ -56,12 +59,17 @@ public class JreCompat {
     private static final boolean graalAvailable;
     private static final boolean jre9Available;
     private static final boolean jre11Available;
+    private static final boolean jre12Available;
     private static final boolean jre16Available;
     private static final boolean jre19Available;
     private static final boolean jre21Available;
     private static final boolean jre22Available;
 
-    private static final Field useCanonCachesField;
+    protected static final String USE_CANON_CACHES_CMD_ARG = 
"-Dsun.io.useCanonCaches=";
+    protected static volatile Boolean canonCachesDisabled;
+    protected static final Object canonCachesDisabledLock = new Object();
+    protected static volatile Optional<Field> useCanonCachesField;
+    protected static final Object useCanonCachesFieldLock = new Object();
     protected static final Method setApplicationProtocolsMethod;
     protected static final Method getApplicationProtocolMethod;
 
@@ -85,6 +93,7 @@ public class JreCompat {
             jre21Available = true;
             jre19Available = true;
             jre16Available = true;
+            jre12Available = true;
             jre9Available = true;
         } else if (Jre21Compat.isSupported()) {
             instance = new Jre21Compat();
@@ -92,6 +101,7 @@ public class JreCompat {
             jre21Available = true;
             jre19Available = true;
             jre16Available = true;
+            jre12Available = true;
             jre9Available = true;
         } else if (Jre19Compat.isSupported()) {
             instance = new Jre19Compat();
@@ -99,6 +109,7 @@ public class JreCompat {
             jre21Available = false;
             jre19Available = true;
             jre16Available = true;
+            jre12Available = true;
             jre9Available = true;
         } else if (Jre16Compat.isSupported()) {
             instance = new Jre16Compat();
@@ -106,6 +117,15 @@ public class JreCompat {
             jre21Available = false;
             jre19Available = false;
             jre16Available = true;
+            jre12Available = true;
+            jre9Available = true;
+        } else if (Jre12Compat.isSupported()) {
+            instance = new Jre12Compat();
+            jre22Available = false;
+            jre21Available = false;
+            jre19Available = false;
+            jre16Available = false;
+            jre12Available = true;
             jre9Available = true;
         } else if (Jre9Compat.isSupported()) {
             instance = new Jre9Compat();
@@ -113,6 +133,7 @@ public class JreCompat {
             jre21Available = false;
             jre19Available = false;
             jre16Available = false;
+            jre12Available = false;
             jre9Available = true;
         } else {
             instance = new JreCompat();
@@ -120,25 +141,11 @@ public class JreCompat {
             jre21Available = false;
             jre19Available = false;
             jre16Available = false;
+            jre12Available = false;
             jre9Available = false;
         }
         jre11Available = instance.jarFileRuntimeMajorVersion() >= 11;
 
-        Field f1 = null;
-        try {
-            Class<?> clazz = Class.forName("java.io.FileSystem");
-            f1 = clazz.getDeclaredField("useCanonCaches");
-            f1.setAccessible(true);
-        } catch (ReflectiveOperationException | RuntimeException e) {
-            /*
-             * Log at debug level as this will only be an issue if the field 
needs to be accessed and most
-             * configurations will not need to do so. Appropriate warnings 
will be logged if an attempt is made to use
-             * the field when it could not be found/accessed.
-             */
-            log.debug(sm.getString("jreCompat.useCanonCaches.init"), e);
-        }
-        useCanonCachesField = f1;
-
         Method m1 = null;
         Method m2 = null;
         try {
@@ -177,6 +184,11 @@ public class JreCompat {
     }
 
 
+    public static boolean isJre12Available() {
+        return jre12Available;
+    }
+
+
     public static boolean isJre16Available() {
         return jre16Available;
     }
@@ -505,28 +517,56 @@ public class JreCompat {
 
 
     /*
-     * The behaviour of the canonical file cache varies by Java version.
+     * The behaviour of the canonical file name cache varies by Java version.
      *
      * The cache was removed in Java 21 so these methods and the associated 
code can be removed once the minimum Java
      * version is 21.
      *
-     * For 12 <= Java <= 20, the cache was present but disabled by default. 
Since the user may have changed the default
-     * Tomcat has to assume the cache is enabled unless proven otherwise.
+     * For 12 <= Java <= 20, the cache was present but disabled by default.
      *
      * For Java < 12, the cache was enabled by default. Tomcat assumes the 
cache is enabled unless proven otherwise.
+     *
+     * Tomcat 10.1 has a minimum Java version of 11.
+     *
+     * The static field in java.io.FileSystem will be set before any 
application code gets a chance to run. Therefore,
+     * the value of that field can be determined by looking at the command 
line arguments. This enables us to determine
+     * the status without having using reflection.
+     *
+     * This is Java 11.
      */
     public boolean isCanonCachesDisabled() {
-        if (useCanonCachesField == null) {
-            // No need to log a warning. The warning will be logged when 
trying to disable the cache.
-            return false;
+        if (canonCachesDisabled != null) {
+            return canonCachesDisabled.booleanValue();
         }
-        boolean result = false;
-        try {
-            result = !((Boolean) useCanonCachesField.get(null)).booleanValue();
-        } catch (ReflectiveOperationException e) {
-            // No need to log a warning. The warning will be logged when 
trying to disable the cache.
+        synchronized (canonCachesDisabledLock) {
+            if (canonCachesDisabled != null) {
+                return canonCachesDisabled.booleanValue();
+            }
+
+            boolean cacheEnabled = true;
+            List<String> args = 
ManagementFactory.getRuntimeMXBean().getInputArguments();
+            for (String arg : args) {
+                // To consider the cache disabled
+                // - there must be at least one command line argument that 
disables it
+                // - there must be no command line arguments that enable it
+                if (arg.startsWith(USE_CANON_CACHES_CMD_ARG)) {
+                    String valueAsString = 
arg.substring(USE_CANON_CACHES_CMD_ARG.length());
+                    boolean valueAsBoolean = 
Boolean.valueOf(valueAsString).booleanValue();
+                    if (valueAsBoolean) {
+                        canonCachesDisabled = Boolean.FALSE;
+                        return false;
+                    } else {
+                        cacheEnabled = false;
+                    }
+                }
+            }
+            if (cacheEnabled) {
+                canonCachesDisabled = Boolean.FALSE;
+            } else {
+                canonCachesDisabled = Boolean.TRUE;
+            }
+            return canonCachesDisabled.booleanValue();
         }
-        return result;
     }
 
 
@@ -537,16 +577,50 @@ public class JreCompat {
      *             as a result of this call, otherwise {@code false}
      */
     public boolean disableCanonCaches() {
-        if (useCanonCachesField == null) {
+        ensureUseCanonCachesFieldIsPopulated();
+        if (!useCanonCachesField.isPresent()) {
             log.warn(sm.getString("jreCompat.useCanonCaches.none"));
             return false;
         }
         try {
-            useCanonCachesField.set(null, Boolean.FALSE);
+            useCanonCachesField.get().set(null, Boolean.FALSE);
         } catch (ReflectiveOperationException | IllegalArgumentException e) {
             log.warn(sm.getString("jreCompat.useCanonCaches.failed"), e);
             return false;
         }
+        synchronized (canonCachesDisabledLock) {
+            canonCachesDisabled = Boolean.TRUE;
+        }
         return true;
     }
+
+
+    protected void ensureUseCanonCachesFieldIsPopulated() {
+        if (useCanonCachesField != null) {
+            return;
+        }
+        synchronized (useCanonCachesFieldLock) {
+            if (useCanonCachesField != null) {
+                return;
+            }
+
+            Field f = null;
+            try {
+                Class<?> clazz = Class.forName("java.io.FileSystem");
+                f = clazz.getDeclaredField("useCanonCaches");
+                // Need this because the 'useCanonCaches' field is private 
final
+                f.setAccessible(true);
+            } catch (ReflectiveOperationException | IllegalArgumentException 
e) {
+                // Make sure field is not set.
+                f = null;
+                log.warn(sm.getString("jreCompat.useCanonCaches.init"), e);
+            }
+
+            if (f == null) {
+                useCanonCachesField = Optional.empty();
+            } else {
+                useCanonCachesField = Optional.of(f);
+            }
+        }
+    }
 }
diff --git a/java/org/apache/tomcat/util/compat/LocalStrings.properties 
b/java/org/apache/tomcat/util/compat/LocalStrings.properties
index 4b33b7a4aa..3f95bb4bdf 100644
--- a/java/org/apache/tomcat/util/compat/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/compat/LocalStrings.properties
@@ -16,6 +16,8 @@
 # Do not edit this file directly.
 # To edit translations see: 
https://tomcat.apache.org/getinvolved.html#Translations
 
+jre12Compat.javaPre12=Method not found so assuming code is running on a 
pre-Java 12 JVM
+
 jre16Compat.javaPre16=Class not found so assuming code is running on a 
pre-Java 16 JVM
 jre16Compat.unexpected=Failed to create references to Java 16 classes and 
methods
 
@@ -39,4 +41,5 @@ jreCompat.noUnixDomainSocket=Java Runtime does not support 
Unix domain sockets.
 jreCompat.noVirtualThreads=Java Runtime does not support virtual threads. You 
must use Java 21 or later to use this feature.
 jreCompat.useCanonCaches.failed=Failed to set the 
java.io.FileSystem.useCanonCaches static field
 jreCompat.useCanonCaches.init=Unable to create a reference to the 
java.io.FileSystem.useCanonCaches static field
-jreCompat.useCanonCaches.none=No reference to the 
java.io.FileSystem.useCanonCaches static field available. Enable debug logging 
for more information.
+jreCompat.useCanonCaches.java18=If using Java 18 to Java 20 inclusive, you 
must not enabled the canonical file name cache using 
-Dsun.io.useCanonCaches=true on the command line as Tomcat is unable to disable 
the cache via reflection on those Java versions
+jreCompat.useCanonCaches.none=No reference to the 
java.io.FileSystem.useCanonCaches static field available.
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 0df6e3166c..8248c461b2 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -129,6 +129,10 @@
         Fix a bug in the JRE compatibility detection that incorrectly 
identified
         Java 19 and Java 20 as supporting Java 21 features. (markt)
       </fix>
+      <fix>
+        Improve the checks for exposure to and protection against 
CVE-2024-56337
+        so that reflection is not used unless required. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">
@@ -160,7 +164,7 @@
       <fix>
         <bug>69576</bug>: Avoid possible failure intializing
         <code>JreCompat</code> due to uncaught exception introduced for the
-        check for CVE-2004-56337. (remm)
+        check for CVE-2024-56337. (remm)
       </fix>
     </changelog>
   </subsection>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to