diff --git a/class-inject/src/main/java/datadog/instrument/classinject/ClassInjector.java b/class-inject/src/main/java/datadog/instrument/classinject/ClassInjector.java index c51390f..a04bbfe 100644 --- a/class-inject/src/main/java/datadog/instrument/classinject/ClassInjector.java +++ b/class-inject/src/main/java/datadog/instrument/classinject/ClassInjector.java @@ -290,7 +290,8 @@ public void visitCode() { mv.visitLdcInsn(DefineClassGlue.V8); } - // unpack the UTF-16BE encoded string back into bytecode + // unpack the UTF-16BE encoded glue string back into bytecode with a single JDK call + // note: this approach is safe because we know the string has no unpaired surrogates mv.visitFieldInsn( GETSTATIC, "java/nio/charset/StandardCharsets", "UTF_16BE", "Ljava/nio/charset/Charset;"); mv.visitMethodInsn( diff --git a/class-inject/src/test/java/datadog/instrument/glue/DefineClassGlueGeneratorTest.java b/class-inject/src/test/java/datadog/instrument/glue/DefineClassGlueGeneratorTest.java index a68e0cc..aa20491 100644 --- a/class-inject/src/test/java/datadog/instrument/glue/DefineClassGlueGeneratorTest.java +++ b/class-inject/src/test/java/datadog/instrument/glue/DefineClassGlueGeneratorTest.java @@ -1,5 +1,6 @@ package datadog.instrument.glue; +import static java.nio.charset.StandardCharsets.UTF_16BE; import static org.assertj.core.api.Assertions.assertThat; import java.io.PrintWriter; @@ -22,4 +23,21 @@ void generatedBytecodePassesVerification(String unsafeNamespace) { assertThat(errors.toString()).isEmpty(); } + + /** + * ClassInjector decodes this glue via a raw {@code getBytes(UTF_16BE)} call; confirm the + * generated bytecode packs cleanly into surrogate pairs it can safely round-trip. + */ + @ParameterizedTest + @ValueSource(strings = {"sun/misc", "jdk/internal/misc"}) + void generatedBytecodeSurvivesRawUtf16beDecode(String unsafeNamespace) { + byte[] originalBytecode = DefineClassGlueGenerator.generateBytecode(unsafeNamespace); + String packedBytecode = + "sun/misc".equals(unsafeNamespace) ? DefineClassGlue.V8 : DefineClassGlue.V9; + + // mirrors exactly what ClassInjector's generated bytecode does at runtime + byte[] decodedBytecode = packedBytecode.getBytes(UTF_16BE); + + assertThat(decodedBytecode).isEqualTo(originalBytecode); + } } diff --git a/utils/src/main/java/datadog/instrument/glue/Glue.java b/utils/src/main/java/datadog/instrument/glue/Glue.java index 65c3824..2f34374 100644 --- a/utils/src/main/java/datadog/instrument/glue/Glue.java +++ b/utils/src/main/java/datadog/instrument/glue/Glue.java @@ -6,7 +6,6 @@ package datadog.instrument.glue; -import static java.nio.charset.StandardCharsets.UTF_16BE; import static java.util.Objects.requireNonNull; import datadog.instrument.utils.JVM; @@ -26,7 +25,14 @@ private Glue() {} * @return the unpacked bytecode */ public static byte[] unpackBytecode(String bytecode) { - return bytecode.getBytes(UTF_16BE); + int len = bytecode.length(); + byte[] unpacked = new byte[len << 1]; + for (int i = 0, j = 0; i < len; i++) { + char c = bytecode.charAt(i); + unpacked[j++] = (byte) (c >>> 8); + unpacked[j++] = (byte) c; + } + return unpacked; } /** diff --git a/utils/src/test/java/datadog/instrument/glue/GlueTest.java b/utils/src/test/java/datadog/instrument/glue/GlueTest.java index 7959d43..cf71e63 100644 --- a/utils/src/test/java/datadog/instrument/glue/GlueTest.java +++ b/utils/src/test/java/datadog/instrument/glue/GlueTest.java @@ -27,4 +27,13 @@ void missingGlueResource() { assertThrows( MissingResourceException.class, () -> Glue.loadBytecode(GlueTest.class, "missing.glue")); } + + @Test + void unpackBytecodePreservesUnpairedSurrogates() { + // getBytes(UTF_16BE) would replace this lone surrogate with U+FFFD instead of preserving it + String packed = new String(new char[] {0x0102, 0xD800, 0x0304}); + byte[] expected = {0x01, 0x02, (byte) 0xD8, 0x00, 0x03, 0x04}; + + assertArrayEquals(expected, Glue.unpackBytecode(packed)); + } }