Mercurial > hg > icedtea7-forest > jdk
changeset 8813:c434c67b8189 jdk7u91-b02
8142882: rebinding of the receiver of a DirectMethodHandle may allow a protected method to be accessed
Reviewed-by: jrose, andrew
author | rasbold |
---|---|
date | Fri, 13 Nov 2015 01:42:37 +0000 |
parents | 12d1f39ed743 |
children | ab44843d5891 |
files | src/share/classes/java/lang/invoke/DirectMethodHandle.java test/java/lang/invoke/ProtectedMethodHandleTest.java |
diffstat | 2 files changed, 193 insertions(+), 37 deletions(-) [+] |
line wrap: on
line diff
--- a/src/share/classes/java/lang/invoke/DirectMethodHandle.java Tue Oct 20 23:03:58 2015 +0100 +++ b/src/share/classes/java/lang/invoke/DirectMethodHandle.java Fri Nov 13 01:42:37 2015 +0000 @@ -131,45 +131,8 @@ return member; } - @Override - MethodHandle bindArgument(int pos, char basicType, Object value) { - // If the member needs dispatching, do so. - if (pos == 0 && basicType == 'L') { - DirectMethodHandle concrete = maybeRebind(value); - if (concrete != null) - return concrete.bindReceiver(value); - } - return super.bindArgument(pos, basicType, value); - } - - @Override - MethodHandle bindReceiver(Object receiver) { - // If the member needs dispatching, do so. - DirectMethodHandle concrete = maybeRebind(receiver); - if (concrete != null) - return concrete.bindReceiver(receiver); - return super.bindReceiver(receiver); - } - private static final MemberName.Factory IMPL_NAMES = MemberName.getFactory(); - private DirectMethodHandle maybeRebind(Object receiver) { - if (receiver != null) { - switch (member.getReferenceKind()) { - case REF_invokeInterface: - case REF_invokeVirtual: - // Pre-dispatch the member. - Class<?> concreteClass = receiver.getClass(); - MemberName concrete = new MemberName(concreteClass, member.getName(), member.getMethodType(), REF_invokeSpecial); - concrete = IMPL_NAMES.resolveOrNull(REF_invokeSpecial, concrete, concreteClass); - if (concrete != null) - return new DirectMethodHandle(type(), preparedLambdaForm(concrete), concrete); - break; - } - } - return null; - } - /** * Create a LF which can invoke the given method. * Cache and share this structure among all methods with
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/java/lang/invoke/ProtectedMethodHandleTest.java Fri Nov 13 01:42:37 2015 +0000 @@ -0,0 +1,193 @@ +/* + * Copyright 2015 Google, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* @test + * @summary check that a MethodHandle can not be used to call a protected interface method + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.nio.charset.StandardCharsets; +import java.security.AllPermission; +import java.security.Permissions; +import java.security.ProtectionDomain; +import java.util.Arrays; +import java.util.HashMap; + +/** + * Tests that a MethodHandle for an interface method (which is necessarily public) cannot be used + * to access a protected method with the same signature. The specific exploit works like this. + * We make a subclass of ClassLoader that implements an interface with a public method matching + * the protected {@link ClassLoader#defineClass(String, byte[], int, int, ProtectionDomain)} method. + * We make a MethodHandle for the interface method, and invoke {@code ClassLoader.defineClass} + * through it. That should not work, of course, but when this test was written it did. + * + * <p>A fair amount of trickery is needed for this test to work. Our subclass of + * ClassLoader cannot be written to implement an interface with the {@code defineClass} method. + * The compiler will reject that, because the method is public and abstract in the interface, but + * its implementation, inherited from {@code ClassLoader}, is protected and final. So we write + * the interface with a method called {@code defineClazz}, and we use a second ClassLoader which + * rewrites the method name to be {@code defineClass}. + * + * @author emcmanus@google.com (Eamonn McManus) + */ +public class ProtectedMethodHandleTest { + /** + * The interface that redefines the {@code defineClass} method from ClassLoader. As explained + * above, this will be rewritten during class loading so that the spelling is indeed + * defineClass and not defineClazz. + */ + public interface DefineClass { + Class<?> defineClazz( + String name, byte[] b, int off, int len, ProtectionDomain protectionDomain); + } + + /** + * If the exploit works, this class will be loaded with a ProtectionDomain + * that has AllPermission. + */ + public static class VanillaClass { + } + + /** + * Loads the class-file bytes of the given class using the standard getResourceAsStream trick. + */ + static byte[] loadClassBytes(Class<?> c) { + String resourceName = c.getName().replace('.', '/') + ".class"; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream inputStream = ProtectedMethodHandleTest.class.getClassLoader() + .getResourceAsStream(resourceName)) { + if (inputStream == null) { + throw new RuntimeException("No such resource: " + resourceName); + } + int b; + while ((b = inputStream.read()) >= 0) { + baos.write(b); + } + baos.flush(); + return baos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * The bytes of every class that we are interested in loading explicitly. This is public because + * it will be accessed from classes loaded by a different loader. + */ + public static final HashMap<String, byte[]> CLASS_BYTES = new HashMap<>(); + + /** + * The loader that launches the exploit code. We use {@link TweakLoader} below to make a copy + * of this class where the DefineClass interface contains a method called {@code defineClass} + * with the same signature as a method from {@code ClassLoader}. + */ + public static class Loader extends ClassLoader implements Runnable, DefineClass { + @Override + public void run() { + try { + doHack(); + throw new RuntimeException("Should have got an exception, but did not."); + } catch (IllegalAccessError expected) { + // Catch and silently swallow the desired IllegalAccessError. + } catch (NullPointerException npe) { + // An otherwise unpatched JDK7 may instead throw an NPE at the + // invoke in doHack below. Though the behavior is confusing, and not + // the preferred exception, it is indicative that the hole is closed. + } catch (Throwable t) { + // Don't catch anything else + throw new RuntimeException(t); + } + } + + private void doHack() throws Throwable { + MethodType methodType = MethodType.methodType( + Class.class, String.class, byte[].class, int.class, int.class, ProtectionDomain.class); + MethodHandle defineClass = MethodHandles.lookup() + .findVirtual(DefineClass.class, "defineClass", methodType) + .bindTo(this); + Permissions allPermissions = new Permissions(); + allPermissions.add(new AllPermission()); + ProtectionDomain protectionDomain = new ProtectionDomain(null, allPermissions); + byte[] vanillaClassBytes = CLASS_BYTES.get(VanillaClass.class.getName()); + Class<?> vanillaClassClass = (Class<?>) defineClass.invoke( + VanillaClass.class.getName(), + vanillaClassBytes, + 0, + vanillaClassBytes.length, + protectionDomain); + // If we have reached this point then the exploit has succeeded: we have called defineClass + // with a ProtectionDomain, and got back a Class that has AllPermission. + } + + // This method is only defined so that this class will compile. It is never called. + @Override + public Class<?> defineClazz(String name, byte[] b, int off, int len, + ProtectionDomain protectionDomain) { + throw new UnsupportedOperationException(); + } + } + + /** + * A ClassLoader that loads the classes in {@link #CLASS_BYTES} using the bytes there, and + * delegates all other class loading to its parent. Additionally, it rewrites the method + * {@link DefineClass#defineClazz} so it is called {@code defineClass}. + */ + public static class TweakLoader extends ClassLoader { + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + byte[] bytes = CLASS_BYTES.get(name); + if (bytes == null) { + return Class.forName(name, resolve, getParent()); + } + if (name.equals(DefineClass.class.getName())) { + byte[] defineClazz = "defineClazz".getBytes(StandardCharsets.UTF_8); + int defineClazzLength = defineClazz.length; + for (int i = 0; i < bytes.length - defineClazzLength; i++) { + if (Arrays.equals(defineClazz, Arrays.copyOfRange(bytes, i, i + defineClazzLength))) { + bytes[i + defineClazzLength - 1] = 's'; + bytes[i + defineClazzLength - 2] = 's'; + } + } + } + Class<?> defineClassClass = defineClass(name, bytes, 0, bytes.length); + return defineClassClass; + } + } + + public static void main(String[] args) throws Exception { + CLASS_BYTES.put(VanillaClass.class.getName(), loadClassBytes(VanillaClass.class)); + CLASS_BYTES.put(DefineClass.class.getName(), loadClassBytes(DefineClass.class)); + CLASS_BYTES.put(Loader.class.getName(), loadClassBytes(Loader.class)); + + // Get a version of Loader where the DefineClass interface has a method defineClass. + Class<?> loaderClass = Class.forName(Loader.class.getName(), false, new TweakLoader()); + + Runnable loader = (Runnable) loaderClass.newInstance(); + loader.run(); + } +}