From 396b4421bdef2368ce96938c40805ad9dc150e29 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Sat, 17 Oct 2026 17:17:28 +0100 Subject: [PATCH] Fix class-name filter checks for Unicode and long names --- .../instrument/utils/ClassNameFilter.java | 22 +++++----- .../instrument/utils/ClassNameFilterTest.java | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/utils/src/main/java/datadog/instrument/utils/ClassNameFilter.java b/utils/src/main/java/datadog/instrument/utils/ClassNameFilter.java index c301309..e38dba7 100644 --- a/utils/src/main/java/datadog/instrument/utils/ClassNameFilter.java +++ b/utils/src/main/java/datadog/instrument/utils/ClassNameFilter.java @@ -154,22 +154,22 @@ public static ClassNameFilter readFrom(DataInput in) throws IOException { * Computes a 32-bit 'class-code' that includes the length of the package-prefix and simple-name, * plus the first and last characters of the simple-name (each truncated to fit into 8-bits.) */ - private static int classCode(String className) { - int start = className.lastIndexOf('.') + 1; - int end = className.length() - 1; - return (0xFF & className.charAt(end)) << 24 - | (0xFF & className.charAt(start)) << 16 + private static int classCode(String name) { + int start = name.lastIndexOf('.') + 1; + int end = name.length() - 1; + return (0xFF & name.charAt(end)) << 24 + | (0xFF & name.charAt(start)) << 16 | (0xFF & (end - start)) << 8 | (0xFF & start); } /** Checks whether the 32-bit 'class-code' is consistent with the fully-qualified class-name. */ - private static boolean checkClassCode(CharSequence className, int code) { - int start = (0xFF & code); - int end = className.length() - 1; - return end - start == (0xFF & (code >> 8)) - && className.charAt(start) == (0xFF & (code >> 16)) - && className.charAt(end) == (0xFF & (code >> 24)); + private static boolean checkClassCode(CharSequence name, int code) { + int start = 0xFF & code; + int end = name.length() - 1; + // Keep the sign bit so a start beyond the end cannot match the encoded length. + return (0x800000FF & (end - start)) == (0xFF & (code >> 8)) + && (((name.charAt(start) ^ code >>> 16) | (name.charAt(end) ^ code >>> 24)) & 0xFF) == 0; } private static int rehash(int oldHash) { diff --git a/utils/src/test/java/datadog/instrument/utils/ClassNameFilterTest.java b/utils/src/test/java/datadog/instrument/utils/ClassNameFilterTest.java index 77530ee..aedbdcf 100644 --- a/utils/src/test/java/datadog/instrument/utils/ClassNameFilterTest.java +++ b/utils/src/test/java/datadog/instrument/utils/ClassNameFilterTest.java @@ -12,12 +12,52 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.util.Arrays; import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class ClassNameFilterTest { + @ParameterizedTest + @ValueSource(strings = {"example.例", "example.例Class", "example.My例"}) + void unicodeClassNames(String className) { + ClassNameFilter filter = new ClassNameFilter(256); + + filter.add(className); + + assertTrue(filter.contains(className)); + } + + @ParameterizedTest + @ValueSource(ints = {256, 257, 512, 513}) + void longSimpleNames(int length) { + char[] simpleName = new char[length]; + Arrays.fill(simpleName, 'A'); + String className = "example." + new String(simpleName); + ClassNameFilter filter = new ClassNameFilter(256); + + filter.add(className); + + assertTrue(filter.contains(className)); + } + + @Test + void shorterHashCollisionDoesNotReadPastClassName() { + char[] padding = new char[248]; + Arrays.fill(padding, 'A'); + String className = "a." + new String(padding) + "DVYRQBJ"; + assertEquals("A".hashCode(), className.hashCode()); + ClassNameFilter filter = new ClassNameFilter(256); + + filter.add(className); + + assertFalse(filter.contains("A")); + assertTrue(filter.contains(className)); + } + @Test void basicOperation() { ClassNameFilter filter = new ClassNameFilter(8 * 1024 * 1024);