Patcher for AWS SDKv2 locale-dependent formatting (#126326)

AWS SDK v2 has a bug (aws/aws-sdk-java-v2#5968) where PathResolver uses locale-dependent formatting.

This PR adds a patcher to the discovery-ec2 build process to replace calls to String.format(<format>, <args>) with String.format(Locale.ROOT, <format>, <args>).

Relates to ES-11279
This commit is contained in:
Lorenzo Dematté 2025-04-15 12:49:56 +02:00 committed by GitHub
parent 07cb14e7a9
commit 2697a3a872
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 187 additions and 2 deletions

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.dependencies.patches.awsv2sdk;
import org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo;
import org.elasticsearch.gradle.internal.dependencies.patches.Utils;
import org.gradle.api.artifacts.transform.CacheableTransform;
import org.gradle.api.artifacts.transform.InputArtifact;
import org.gradle.api.artifacts.transform.TransformAction;
import org.gradle.api.artifacts.transform.TransformOutputs;
import org.gradle.api.artifacts.transform.TransformParameters;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Classpath;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.List;
import static org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo.classPatcher;
@CacheableTransform
public abstract class Awsv2ClassPatcher implements TransformAction<TransformParameters.None> {
private static final String JAR_FILE_TO_PATCH = "aws-query-protocol";
private static final List<PatcherInfo> CLASS_PATCHERS = List.of(
// This patcher is needed because of this AWS bug: https://github.com/aws/aws-sdk-java-v2/issues/5968
// As soon as the bug is resolved and we upgrade our AWS SDK v2 libraries, we can remove this.
classPatcher(
"software/amazon/awssdk/protocols/query/internal/marshall/ListQueryMarshaller.class",
"213e84d9a745bdae4b844334d17aecdd6499b36df32aa73f82dc114b35043009",
StringFormatInPathResolverPatcher::new
)
);
@Classpath
@InputArtifact
public abstract Provider<FileSystemLocation> getInputArtifact();
@Override
public void transform(@NotNull TransformOutputs outputs) {
File inputFile = getInputArtifact().get().getAsFile();
if (inputFile.getName().startsWith(JAR_FILE_TO_PATCH)) {
System.out.println("Patching " + inputFile.getName());
File outputFile = outputs.file(inputFile.getName().replace(".jar", "-patched.jar"));
Utils.patchJar(inputFile, outputFile, CLASS_PATCHERS);
} else {
System.out.println("Skipping " + inputFile.getName());
outputs.file(getInputArtifact());
}
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.dependencies.patches.awsv2sdk;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import java.util.Locale;
import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
class StringFormatInPathResolverPatcher extends ClassVisitor {
StringFormatInPathResolverPatcher(ClassWriter classWriter) {
super(ASM9, classWriter);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return new ReplaceCallMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions));
}
/**
* Replaces calls to String.format(format, args); with calls to String.format(Locale.ROOT, format, args);
*/
private static class ReplaceCallMethodVisitor extends MethodVisitor {
private static final String CLASS_INTERNAL_NAME = Type.getInternalName(String.class);
private static final String METHOD_NAME = "format";
private static final String OLD_METHOD_DESCRIPTOR = Type.getMethodDescriptor(
Type.getType(String.class),
Type.getType(String.class),
Type.getType(Object[].class)
);
private static final String NEW_METHOD_DESCRIPTOR = Type.getMethodDescriptor(
Type.getType(String.class),
Type.getType(Locale.class),
Type.getType(String.class),
Type.getType(Object[].class)
);
private boolean foundFormatPattern = false;
ReplaceCallMethodVisitor(MethodVisitor methodVisitor) {
super(ASM9, methodVisitor);
}
@Override
public void visitLdcInsn(Object value) {
if (value instanceof String s && s.startsWith("%s")) {
if (foundFormatPattern) {
throw new IllegalStateException(
"A previous string format constant was not paired with a String.format() call. "
+ "Patching would generate an unbalances stack"
);
}
// Push the extra arg on the stack
mv.visitFieldInsn(GETSTATIC, Type.getInternalName(Locale.class), "ROOT", Type.getDescriptor(Locale.class));
foundFormatPattern = true;
}
super.visitLdcInsn(value);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (opcode == INVOKESTATIC
&& foundFormatPattern
&& CLASS_INTERNAL_NAME.equals(owner)
&& METHOD_NAME.equals(name)
&& OLD_METHOD_DESCRIPTOR.equals(descriptor)) {
// Replace the call with String.format(Locale.ROOT, format, args)
mv.visitMethodInsn(INVOKESTATIC, CLASS_INTERNAL_NAME, METHOD_NAME, NEW_METHOD_DESCRIPTOR, false);
foundFormatPattern = false;
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
}
}

View file

@ -15,6 +15,31 @@ esplugin {
classname ='org.elasticsearch.discovery.ec2.Ec2DiscoveryPlugin'
}
def patched = Attribute.of('patched', Boolean)
configurations {
compileClasspath {
attributes {
attribute(patched, true)
}
}
runtimeClasspath {
attributes {
attribute(patched, true)
}
}
testCompileClasspath {
attributes {
attribute(patched, true)
}
}
testRuntimeClasspath {
attributes {
attribute(patched, true)
}
}
}
dependencies {
implementation "software.amazon.awssdk:annotations:${versions.awsv2sdk}"
@ -65,6 +90,17 @@ dependencies {
testImplementation project(':test:fixtures:ec2-imds-fixture')
internalClusterTestImplementation project(':test:fixtures:ec2-imds-fixture')
attributesSchema {
attribute(patched)
}
artifactTypes.getByName("jar") {
attributes.attribute(patched, false)
}
registerTransform(org.elasticsearch.gradle.internal.dependencies.patches.awsv2sdk.Awsv2ClassPatcher) {
from.attribute(patched, false)
to.attribute(patched, true)
}
}
tasks.named("dependencyLicenses").configure {

View file

@ -103,8 +103,7 @@ public class Ec2DiscoveryTests extends AbstractEC2MockAPITestCase {
final String[] params = request.split("&");
Arrays.stream(params).filter(entry -> entry.startsWith("Filter.") && entry.contains("=tag%3A")).forEach(entry -> {
final int startIndex = "Filter.".length();
// TODO ensure the filterId is an ASCII int when https://github.com/aws/aws-sdk-java-v2/issues/5968 fixed
final var filterId = entry.substring(startIndex, entry.indexOf(".", startIndex));
final int filterId = Integer.parseInt(entry.substring(startIndex, entry.indexOf(".", startIndex)));
tagsIncluded.put(
entry.substring(entry.indexOf("=tag%3A") + "=tag%3A".length()),
Arrays.stream(params)