From b20a6b0c137588cfb5d4cdb4a3d489f6ac93154e Mon Sep 17 00:00:00 2001 From: Dale Emery Date: Wed, 8 Jun 2022 14:17:49 -0700 Subject: [PATCH] GEODE-10321: Acceptance test for Geode access to JDK internals (#7772) * GEODE-10321: Acceptance test for Geode access to JDK internals Add acceptance tests to demonstrate two ways to give Geode access to encapsulated fields of a JDK 17 class: - Tell Gfsh to open the class's package when starting a Geode member. - Tell Gfsh to use `open-all-jdk-packages-linux-openjdk-17` as an argument file when starting a Geode member. Add another test to demonstrate the kind of error that happens when Geode cannot access an encapsulated field of a JDK 17 class. * Clean up Remove unnecessary dependency on JUnit 5 rule migration support. Start server without default server. Other cleanup. --- .../geode/jdk/JdkEncapsulationTest.java | 124 ++++++++++++++++++ .../jdk/TraverseEncapsulatedJdkObject.java | 52 ++++++++ .../org/apache/geode/test/util/JarUtils.java | 59 +++++++++ 3 files changed, 235 insertions(+) create mode 100644 geode-assembly/src/acceptanceTest/java/org/apache/geode/jdk/JdkEncapsulationTest.java create mode 100644 geode-assembly/src/acceptanceTest/java/org/apache/geode/jdk/TraverseEncapsulatedJdkObject.java create mode 100644 geode-junit/src/main/java/org/apache/geode/test/util/JarUtils.java diff --git a/geode-assembly/src/acceptanceTest/java/org/apache/geode/jdk/JdkEncapsulationTest.java b/geode-assembly/src/acceptanceTest/java/org/apache/geode/jdk/JdkEncapsulationTest.java new file mode 100644 index 000000000000..e500f5e625c9 --- /dev/null +++ b/geode-assembly/src/acceptanceTest/java/org/apache/geode/jdk/JdkEncapsulationTest.java @@ -0,0 +1,124 @@ +/* + * 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.geode.jdk; + +import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPort; +import static org.apache.geode.test.util.JarUtils.createJarWithClasses; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import org.apache.geode.test.junit.rules.FolderRule; +import org.apache.geode.test.junit.rules.gfsh.GfshRule; +import org.apache.geode.test.junit.rules.gfsh.GfshScript; +import org.apache.geode.test.version.JavaVersions; + +/** + * Test several ways to make normally inaccessible JDK packages accessible on JDK 17. + */ +public class JdkEncapsulationTest { + @Rule(order = 0) + public final FolderRule folderRule = new FolderRule(); + + @Rule(order = 1) + public final GfshRule gfshRule = new GfshRule(folderRule::getFolder); + + private String startServer; + private GfshScript traverseEncapsulatedJdkObject; + + @BeforeClass + public static void validOnlyOnJdk17AndLater() { + assumeThat(JavaVersions.current().specificationVersion()) + .isGreaterThanOrEqualTo(17); + } + + @Before + public void startLocatorWithObjectTraverserFunction() throws IOException { + Path jarPath = folderRule.getFolder().toPath().resolve("traverse-encapsulated-jdk-object.jar"); + createJarWithClasses(jarPath, TraverseEncapsulatedJdkObject.class); + + int locatorPort = getRandomAvailableTCPPort(); + String locators = "localhost[" + locatorPort + "]"; + + startServer = "start server --name=server --disable-default-server --locators=" + locators; + traverseEncapsulatedJdkObject = GfshScript + .of("connect --locator=" + locators) + .and("execute function --id=" + TraverseEncapsulatedJdkObject.ID); + + GfshScript + .of("start locator --port=" + locatorPort) + .and("deploy --jar=" + jarPath) + .execute(gfshRule); + } + + // If this test fails, it means the object we're trying to traverse has no inaccessible fields, + // and so is not useful for the other tests. If it fails, update TraverseInaccessibleJdkObject + // to use a type that actually has inaccessible fields. + @Test + public void cannotMakeEncapsulatedFieldsAccessibleByDefault() { + gfshRule.execute(startServer); // No JDK options + + String traversalResult = traverseEncapsulatedJdkObject + .expectExitCode(1) // Because we did not open any JDK packages. + .execute(gfshRule) + .getOutputText(); + + assertThat(traversalResult) + .as("result of traversing %s", TraverseEncapsulatedJdkObject.OBJECT.getClass()) + .contains("Exception: java.lang.reflect.InaccessibleObjectException"); + } + + @Test + public void canMakeEncapsulatedFieldsAccessibleInExplicitlyOpenedPackages() { + String objectPackage = TraverseEncapsulatedJdkObject.OBJECT.getClass().getPackage().getName(); + String objectModule = TraverseEncapsulatedJdkObject.MODULE; + + String openThePackageOfTheEncapsulatedJdkObject = + String.format(" --J=--add-opens=%s/%s=ALL-UNNAMED", objectModule, objectPackage); + + gfshRule.execute(startServer + openThePackageOfTheEncapsulatedJdkObject); + + traverseEncapsulatedJdkObject + .expectExitCode(0) // Because we opened the encapsulated object's package. + .execute(gfshRule); + } + + @Test + public void canMakeEncapsulatedFieldsAccessibleInPackagesOpenedByArgumentFile() { + // A few of the packages opened by this argument file are specific to Linux JDKs. Running this + // test on other operating systems might emit some warnings, but they are harmless. + String argumentFileName = "open-all-jdk-packages-linux-openjdk-17"; + Path argumentFilePath = Paths.get(System.getenv("GEODE_HOME"), "config", argumentFileName) + .toAbsolutePath().normalize(); + + String useArgumentFile = " --J=@" + argumentFilePath; + + gfshRule.execute(startServer + useArgumentFile); + + traverseEncapsulatedJdkObject + .expectExitCode(0) // Because the argument file opens all JDK packages. + .execute(gfshRule); + } +} diff --git a/geode-assembly/src/acceptanceTest/java/org/apache/geode/jdk/TraverseEncapsulatedJdkObject.java b/geode-assembly/src/acceptanceTest/java/org/apache/geode/jdk/TraverseEncapsulatedJdkObject.java new file mode 100644 index 000000000000..a2239d3116e8 --- /dev/null +++ b/geode-assembly/src/acceptanceTest/java/org/apache/geode/jdk/TraverseEncapsulatedJdkObject.java @@ -0,0 +1,52 @@ +/* + * 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.geode.jdk; + +import java.math.BigDecimal; + +import org.apache.geode.cache.execute.Function; +import org.apache.geode.cache.execute.FunctionContext; +import org.apache.geode.internal.size.ObjectTraverser; +import org.apache.geode.internal.size.ObjectTraverser.Visitor; + +public class TraverseEncapsulatedJdkObject implements Function { + private static final Visitor TRAVERSE_ENTIRE_OBJECT_GRAPH = (parent, object) -> true; + private final ObjectTraverser traverser = new ObjectTraverser(); + + // OBJECT must have a JDK type with inaccessible fields, defined in a package that Gfsh does + // not open by default. + static final BigDecimal OBJECT = BigDecimal.ONE; + // MODULE must be the module that defines OBJECT's type. + static final String MODULE = "java.base"; + static final String ID = "traverse-big-decimal"; + + @Override + public String getId() { + return ID; + } + + @Override + public void execute(FunctionContext context) { + try { + traverser.breadthFirstSearch(OBJECT, TRAVERSE_ENTIRE_OBJECT_GRAPH, false); + } catch (IllegalAccessException e) { + context.getResultSender().sendException(e); + return; + } + context.getResultSender().lastResult("OK"); + } +} diff --git a/geode-junit/src/main/java/org/apache/geode/test/util/JarUtils.java b/geode-junit/src/main/java/org/apache/geode/test/util/JarUtils.java new file mode 100644 index 000000000000..09fa996ec2ba --- /dev/null +++ b/geode-junit/src/main/java/org/apache/geode/test/util/JarUtils.java @@ -0,0 +1,59 @@ +/* + * 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.geode.test.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import org.apache.commons.io.IOUtils; + +public class JarUtils { + public static Path createJarWithClasses(Path jarPath, Class... classes) throws IOException { + Path jarFile = Files.createFile(jarPath); + try (OutputStream outputStream = Files.newOutputStream(jarFile); + JarOutputStream jarOutputStream = new JarOutputStream(outputStream)) { + Arrays.stream(classes) + .forEach(clazz -> writeClassTo(clazz, jarOutputStream)); + } + return jarFile; + } + + private static void writeClassTo(Class clazz, JarOutputStream jarOutputStream) { + String className = clazz.getName(); + String classAsPath = className.replace('.', '/') + ".class"; + try (InputStream classInputStream = clazz.getClassLoader().getResourceAsStream(classAsPath)) { + if (classInputStream == null) { + throw new IllegalArgumentException("Cannot read class definition for " + className); + } + JarEntry classEntry = new JarEntry(classAsPath); + classEntry.setTime(System.currentTimeMillis()); + byte[] classBytes = IOUtils.toByteArray(classInputStream); + jarOutputStream.putNextEntry(classEntry); + jarOutputStream.write(classBytes); + jarOutputStream.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +}