From a58b45d6226548d16fe8caa79c6ff44d036ca8cc Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 3 Jul 2026 15:50:48 +0200 Subject: [PATCH 1/4] fix(common): strip leading whitespace in ClassPreLoader ClassPreLoader used line.stripTrailing() when parsing classesloaded.txt, which keeps leading whitespace. Modules that ship the file with a leading space on every line (for example powertools-tracing) passed names such as " java.lang.Object" to Class.forName, which throws ClassNotFoundException, so SnapStart class priming loaded 0 classes. Use line.strip() so both leading and trailing whitespace are removed. Add a regression test that references a class with a leading space. Closes #2559 --- .../common/internal/ClassPreLoader.java | 2 +- .../common/internal/ClassPreLoaderTest.java | 25 +++++++++++++++++-- .../src/test/resources/classesloaded.txt | 3 ++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java index ca599429e..34dc35dc2 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java @@ -66,7 +66,7 @@ private static void preloadClassesFromStream(InputStream is) { if (idx != -1) { line = line.substring(0, idx); } - final String className = line.stripTrailing(); + final String className = line.strip(); if (!className.isBlank()) { loadClassIfFound(className); } diff --git a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java index 03991688e..fb1359434 100644 --- a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java +++ b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java @@ -8,6 +8,7 @@ class ClassPreLoaderTest { // Making this volatile so the Thread Context doesn't need any special handling static volatile boolean dummyClassLoaded = false; + static volatile boolean leadingSpaceDummyClassLoaded = false; /** * Dummy class to be loaded by ClassPreLoader in test. @@ -20,15 +21,35 @@ static class DummyClass { dummyClassLoaded = true; } } + + /** + * Dummy class whose name is referenced with a leading space in + * powertools-common/src/test/resources/classesloaded.txt. + * Some modules ship a classesloaded.txt with a leading space on every line, so the loader must + * strip leading whitespace before calling Class.forName. + */ + static class LeadingSpaceDummyClass { + static { + leadingSpaceDummyClassLoaded = true; + } + } + @Test void preloadClasses_shouldIgnoreInvalidClassesAndLoadValidClasses() { dummyClassLoaded = false; - // powertools-common/src/test/resources/classesloaded.txt has a class that does not exist + leadingSpaceDummyClassLoaded = false; + // A class only runs its static initializer the first time it is loaded, so this test calls + // preloadClasses once and asserts on all classes listed in classesloaded.txt together. + // powertools-common/src/test/resources/classesloaded.txt has a class that does not exist. // Verify that the missing class did not throw any exception assertDoesNotThrow(ClassPreLoader::preloadClasses); - // When the classloaded.txt is a mixed bag of valid and invalid classes, Valid class must load + // When the classloaded.txt is a mixed bag of valid and invalid classes, valid classes must load assertTrue(dummyClassLoaded, "DummyClass should be loaded"); + // classesloaded.txt references LeadingSpaceDummyClass with a leading space. The loader must + // strip it before Class.forName, otherwise the class is never loaded. + assertTrue(leadingSpaceDummyClassLoaded, + "LeadingSpaceDummyClass should be loaded despite the leading space"); } } \ No newline at end of file diff --git a/powertools-common/src/test/resources/classesloaded.txt b/powertools-common/src/test/resources/classesloaded.txt index 498ffdf69..a6cda309e 100644 --- a/powertools-common/src/test/resources/classesloaded.txt +++ b/powertools-common/src/test/resources/classesloaded.txt @@ -1,2 +1,3 @@ software.amazon.lambda.powertools.common.internal.NonExistingClass -software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$DummyClass \ No newline at end of file +software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$DummyClass + software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$LeadingSpaceDummyClass From 6f64a17085c85151b163f40eb1cf3d5e0cf10c6d Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 3 Jul 2026 16:18:46 +0200 Subject: [PATCH 2/4] fix(common): reject malformed classesloaded.txt entries and fix tracing test isolation Harden ClassPreLoader to only pass well-formed binary class names to Class.forName. Some modules ship classesloaded.txt with malformed entries such as runtime-synthetic lambda classes and lines that glue a class name to a file path, none of which are loadable. Add a debug log reporting how many classes were preloaded. Fixing the leading-whitespace parsing means the well-formed classes in powertools-tracing now actually load during priming. That exposed a pre-existing test isolation defect: PowerTracerToolEnabledForResponseWithCustomMapper set the static TracingUtils.objectMapper from a static initializer, which leaked into later tests in the same JVM fork and changed how errors were serialized. Set the mapper from the constructor instead and reset it in the aspect test teardown. Relates to #2559 --- .../common/internal/ClassPreLoader.java | 27 ++++++++++++++++--- .../common/internal/ClassPreLoaderTest.java | 18 +++++++++++++ .../src/test/resources/classesloaded.txt | 1 + ...oolEnabledForResponseWithCustomMapper.java | 6 ++++- .../internal/LambdaTracingAspectTest.java | 5 ++++ 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java index 34dc35dc2..5c33a7cba 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java @@ -21,6 +21,10 @@ import java.net.URL; import java.net.URLConnection; import java.util.Enumeration; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Used to preload classes to support automatic priming for SnapStart @@ -28,6 +32,15 @@ public final class ClassPreLoader { public static final String CLASSES_FILE = "classesloaded.txt"; + private static final Logger LOG = LoggerFactory.getLogger(ClassPreLoader.class); + + // A binary class name is a series of dot-separated Java identifiers ($ is allowed for nested + // classes). This filters out malformed entries such as runtime-synthetic lambda classes + // (e.g. "com.example.Foo$$Lambda$1/0x0000...") and lines that contain a path or other junk, + // none of which Class.forName can load. + private static final Pattern BINARY_CLASS_NAME = Pattern.compile( + "[\\p{L}_$][\\p{L}\\p{N}_$]*(\\.[\\p{L}_$][\\p{L}\\p{N}_$]*)*"); + private ClassPreLoader() { // Hide default constructor } @@ -57,6 +70,7 @@ public static void preloadClasses() { * @param is */ private static void preloadClassesFromStream(InputStream is) { + int loaded = 0; try (is; InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { @@ -67,25 +81,30 @@ private static void preloadClassesFromStream(InputStream is) { line = line.substring(0, idx); } final String className = line.strip(); - if (!className.isBlank()) { - loadClassIfFound(className); + if (!className.isBlank() && BINARY_CLASS_NAME.matcher(className).matches() + && loadClassIfFound(className)) { + loaded++; } } } catch (Exception ignored) { // No action is required if preloading fails for any reason } + LOG.debug("SnapStart priming: preloaded {} class(es) from {}", loaded, CLASSES_FILE); } /** * Initializes the class with given name if found, ignores otherwise * - * @param className + * @param className the binary name of the class to load + * @return true if the class was found and loaded, false otherwise */ - private static void loadClassIfFound(String className) { + private static boolean loadClassIfFound(String className) { try { Class.forName(className, true, ClassPreLoader.class.getClassLoader()); + return true; } catch (ClassNotFoundException e) { // No action is required if the class with given name cannot be found + return false; } } } \ No newline at end of file diff --git a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java index fb1359434..f221e4129 100644 --- a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java +++ b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java @@ -9,6 +9,7 @@ class ClassPreLoaderTest { // Making this volatile so the Thread Context doesn't need any special handling static volatile boolean dummyClassLoaded = false; static volatile boolean leadingSpaceDummyClassLoaded = false; + static volatile boolean malformedDummyClassLoaded = false; /** * Dummy class to be loaded by ClassPreLoader in test. @@ -34,11 +35,24 @@ static class LeadingSpaceDummyClass { } } + /** + * Dummy class whose name is referenced by a malformed entry in + * powertools-common/src/test/resources/classesloaded.txt, where the class name is + * followed by a URL-encoded space and a file path. The loader must reject such entries rather + * than pass them to Class.forName. + */ + static class MalformedDummyClass { + static { + malformedDummyClassLoaded = true; + } + } + @Test void preloadClasses_shouldIgnoreInvalidClassesAndLoadValidClasses() { dummyClassLoaded = false; leadingSpaceDummyClassLoaded = false; + malformedDummyClassLoaded = false; // A class only runs its static initializer the first time it is loaded, so this test calls // preloadClasses once and asserts on all classes listed in classesloaded.txt together. // powertools-common/src/test/resources/classesloaded.txt has a class that does not exist. @@ -51,5 +65,9 @@ void preloadClasses_shouldIgnoreInvalidClassesAndLoadValidClasses() { // strip it before Class.forName, otherwise the class is never loaded. assertTrue(leadingSpaceDummyClassLoaded, "LeadingSpaceDummyClass should be loaded despite the leading space"); + // classesloaded.txt references MalformedDummyClass with trailing junk (%20 and a path). The + // loader must reject the malformed entry, so the class must not be loaded. + assertFalse(malformedDummyClassLoaded, + "MalformedDummyClass should not be loaded from a malformed entry"); } } \ No newline at end of file diff --git a/powertools-common/src/test/resources/classesloaded.txt b/powertools-common/src/test/resources/classesloaded.txt index a6cda309e..72f532a7f 100644 --- a/powertools-common/src/test/resources/classesloaded.txt +++ b/powertools-common/src/test/resources/classesloaded.txt @@ -1,3 +1,4 @@ software.amazon.lambda.powertools.common.internal.NonExistingClass software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$DummyClass software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$LeadingSpaceDummyClass + software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$MalformedDummyClass%20SAHA/.m2/repository/some/path.jar diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java index a18b1580d..3627ab355 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java @@ -28,7 +28,11 @@ import software.amazon.lambda.powertools.tracing.TracingUtils; public class PowerTracerToolEnabledForResponseWithCustomMapper implements RequestHandler { - static { + public PowerTracerToolEnabledForResponseWithCustomMapper() { + // Set the custom mapper in the constructor rather than a static initializer. A static + // initializer only runs the first time this class is loaded, which makes the resulting + // global state depend on when the class happens to be loaded (for example by SnapStart + // class priming). Setting it in the constructor makes each instantiation deterministic. ObjectMapper objectMapper = new ObjectMapper(); SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(ChildClass.class, new ChildSerializer()); diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java index b22f7a9af..bf0e9aa90 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java @@ -41,6 +41,7 @@ import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledExplicitlyForResponseAndError; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForError; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForResponse; +import software.amazon.lambda.powertools.tracing.TracingUtils; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForResponseWithCustomMapper; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForStream; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForStreamWithNoMetaData; @@ -70,6 +71,10 @@ void setUp() throws IllegalAccessException { @AfterEach void tearDown() { AWSXRay.endSegment(); + // Some tests (e.g. the custom mapper handler) set the static TracingUtils.objectMapper. + // Reset it so it does not leak into later tests running in the same JVM fork and change how + // responses and errors are serialized. + TracingUtils.defaultObjectMapper(null); } @Test From 1db2df57e563b3b3f71e9ef1fd92eceed2e7b23a Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 3 Jul 2026 16:22:12 +0200 Subject: [PATCH 3/4] fix(common): simplify class name pattern to avoid backtracking Replace the nested-quantifier regex with a single linear character class. SonarCloud flagged the previous pattern for possible catastrophic backtracking on large inputs. The character class rejects the same malformed entries (paths, URL-encoded spaces, synthetic lambda names) without the backtracking risk. --- .../powertools/common/internal/ClassPreLoader.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java index 5c33a7cba..8507d3b74 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java @@ -34,12 +34,12 @@ public final class ClassPreLoader { private static final Logger LOG = LoggerFactory.getLogger(ClassPreLoader.class); - // A binary class name is a series of dot-separated Java identifiers ($ is allowed for nested - // classes). This filters out malformed entries such as runtime-synthetic lambda classes - // (e.g. "com.example.Foo$$Lambda$1/0x0000...") and lines that contain a path or other junk, - // none of which Class.forName can load. - private static final Pattern BINARY_CLASS_NAME = Pattern.compile( - "[\\p{L}_$][\\p{L}\\p{N}_$]*(\\.[\\p{L}_$][\\p{L}\\p{N}_$]*)*"); + // A binary class name contains only letters, digits, and the '.', '_' and '$' characters. This + // filters out malformed entries such as runtime-synthetic lambda classes + // (e.g. "com.example.Foo$$Lambda$1/0x0000...") and lines that contain a path or other junk + // (e.g. a URL-encoded space "%20" followed by a file path), none of which Class.forName can + // load. The character class is a single linear match, so it is not prone to backtracking. + private static final Pattern BINARY_CLASS_NAME = Pattern.compile("[\\p{L}\\p{N}_$.]+"); private ClassPreLoader() { // Hide default constructor From 8cf0c98e068cb97de6f97cc1a20cec4cbd6fb914 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 3 Jul 2026 16:35:07 +0200 Subject: [PATCH 4/4] refactor(common): drop class name pattern, rely on Class.forName Remove the binary class name pattern matching. Class.forName already rejects malformed entries by throwing ClassNotFoundException, which is caught and ignored, so the extra pattern check only duplicated that rejection and added complexity. The tracing test failures were caused by test ordering (a leaked static ObjectMapper), not by malformed entries, and that fix is retained. --- .../common/internal/ClassPreLoader.java | 11 +---------- .../common/internal/ClassPreLoaderTest.java | 18 ------------------ .../src/test/resources/classesloaded.txt | 1 - 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java index 8507d3b74..77318722c 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java @@ -21,7 +21,6 @@ import java.net.URL; import java.net.URLConnection; import java.util.Enumeration; -import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,13 +33,6 @@ public final class ClassPreLoader { private static final Logger LOG = LoggerFactory.getLogger(ClassPreLoader.class); - // A binary class name contains only letters, digits, and the '.', '_' and '$' characters. This - // filters out malformed entries such as runtime-synthetic lambda classes - // (e.g. "com.example.Foo$$Lambda$1/0x0000...") and lines that contain a path or other junk - // (e.g. a URL-encoded space "%20" followed by a file path), none of which Class.forName can - // load. The character class is a single linear match, so it is not prone to backtracking. - private static final Pattern BINARY_CLASS_NAME = Pattern.compile("[\\p{L}\\p{N}_$.]+"); - private ClassPreLoader() { // Hide default constructor } @@ -81,8 +73,7 @@ private static void preloadClassesFromStream(InputStream is) { line = line.substring(0, idx); } final String className = line.strip(); - if (!className.isBlank() && BINARY_CLASS_NAME.matcher(className).matches() - && loadClassIfFound(className)) { + if (!className.isBlank() && loadClassIfFound(className)) { loaded++; } } diff --git a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java index f221e4129..fb1359434 100644 --- a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java +++ b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java @@ -9,7 +9,6 @@ class ClassPreLoaderTest { // Making this volatile so the Thread Context doesn't need any special handling static volatile boolean dummyClassLoaded = false; static volatile boolean leadingSpaceDummyClassLoaded = false; - static volatile boolean malformedDummyClassLoaded = false; /** * Dummy class to be loaded by ClassPreLoader in test. @@ -35,24 +34,11 @@ static class LeadingSpaceDummyClass { } } - /** - * Dummy class whose name is referenced by a malformed entry in - * powertools-common/src/test/resources/classesloaded.txt, where the class name is - * followed by a URL-encoded space and a file path. The loader must reject such entries rather - * than pass them to Class.forName. - */ - static class MalformedDummyClass { - static { - malformedDummyClassLoaded = true; - } - } - @Test void preloadClasses_shouldIgnoreInvalidClassesAndLoadValidClasses() { dummyClassLoaded = false; leadingSpaceDummyClassLoaded = false; - malformedDummyClassLoaded = false; // A class only runs its static initializer the first time it is loaded, so this test calls // preloadClasses once and asserts on all classes listed in classesloaded.txt together. // powertools-common/src/test/resources/classesloaded.txt has a class that does not exist. @@ -65,9 +51,5 @@ void preloadClasses_shouldIgnoreInvalidClassesAndLoadValidClasses() { // strip it before Class.forName, otherwise the class is never loaded. assertTrue(leadingSpaceDummyClassLoaded, "LeadingSpaceDummyClass should be loaded despite the leading space"); - // classesloaded.txt references MalformedDummyClass with trailing junk (%20 and a path). The - // loader must reject the malformed entry, so the class must not be loaded. - assertFalse(malformedDummyClassLoaded, - "MalformedDummyClass should not be loaded from a malformed entry"); } } \ No newline at end of file diff --git a/powertools-common/src/test/resources/classesloaded.txt b/powertools-common/src/test/resources/classesloaded.txt index 72f532a7f..a6cda309e 100644 --- a/powertools-common/src/test/resources/classesloaded.txt +++ b/powertools-common/src/test/resources/classesloaded.txt @@ -1,4 +1,3 @@ software.amazon.lambda.powertools.common.internal.NonExistingClass software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$DummyClass software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$LeadingSpaceDummyClass - software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$MalformedDummyClass%20SAHA/.m2/repository/some/path.jar