1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ondevicepersonalization.libraries.plugin.internal; 18 19 import androidx.annotation.NonNull; 20 21 import com.google.common.collect.ImmutableSet; 22 23 /** 24 * An isolation {@link ClassLoader} layer between plugin container's class loader and plugin's class 25 * loader. The layer mediates class loading requests and responses. By default it only allows 26 * classes at Android framework as well as explicitly specified at the {@link 27 * IsolationClassLoader#containerClassesAllowlist} to be loaded outside plugin's class loader to 28 * secure class loading boundary. 29 */ 30 public final class IsolationClassLoader extends ClassLoader { 31 private final ImmutableSet<String> mContainerClassesAllowlist; 32 private final ImmutableSet<String> mContainerPackagesAllowlist; 33 private ImmutableSet<String> mRejectedClassesList = ImmutableSet.of(); 34 35 /** 36 * Create an isolation layer to manage class loading requests/responses to/from plugin 37 * container's class loader. 38 * 39 * <p>Callers can optionally supply a classes allow list to specify additional classes the layer 40 * allows for passing its isolation boundary. 41 * 42 * @param containerClassLoader The plugin container's class loader to be isolated and managed. 43 * @param containerClassesAllowlist The classes list to allow pass-through of class loading 44 * requests/responses. 45 */ IsolationClassLoader( @onNull ClassLoader containerClassLoader, @NonNull ImmutableSet<String> containerClassesAllowlist, @NonNull ImmutableSet<String> containerPackagesAllowlist)46 public IsolationClassLoader( 47 @NonNull ClassLoader containerClassLoader, 48 @NonNull ImmutableSet<String> containerClassesAllowlist, 49 @NonNull ImmutableSet<String> containerPackagesAllowlist) { 50 super(containerClassLoader); 51 this.mContainerClassesAllowlist = containerClassesAllowlist; 52 this.mContainerPackagesAllowlist = containerPackagesAllowlist; 53 } 54 55 /** 56 * The same as {@link IsolationClassLoader#IsolationClassLoader(ClassLoader, String...)} with an 57 * additional rejected-classes-list to specify what classes should be forbidden for use. 58 * 59 * <p>This is useful for callers to implement their own runtime polices checking if there are 60 * any disallowed java classes being used unexpectedly in code. E.g., a policy does not allow 61 * thread creation nor tasks scheduled in different thread context, simply specify {@link 62 * java.util.concurrent.ExecutorService} and {@link Thread} in rejectedClassesList to fulfill 63 * the policy requirement. 64 * 65 * @param containerClassLoader The plugin container's class loader to be isolated and managed. 66 * @param rejectedClassesList The classes list to be rejected if being used. 67 * @param containerClassesAllowlist The extra classes list to allow pass-through of * class 68 * loading requests/responses. 69 */ IsolationClassLoader( @onNull ClassLoader containerClassLoader, @NonNull ImmutableSet<String> containerClassesAllowlist, @NonNull ImmutableSet<String> containerPackagesAllowlist, @NonNull ImmutableSet<String> rejectedClassesList)70 public IsolationClassLoader( 71 @NonNull ClassLoader containerClassLoader, 72 @NonNull ImmutableSet<String> containerClassesAllowlist, 73 @NonNull ImmutableSet<String> containerPackagesAllowlist, 74 @NonNull ImmutableSet<String> rejectedClassesList) { 75 this(containerClassLoader, containerClassesAllowlist, containerPackagesAllowlist); 76 this.mRejectedClassesList = rejectedClassesList; 77 } 78 startsWithAnyPrefixFromSet(String name, ImmutableSet<String> prefixes)79 private static boolean startsWithAnyPrefixFromSet(String name, ImmutableSet<String> prefixes) { 80 for (String prefix : prefixes) { 81 if (name.startsWith(prefix)) { 82 return true; 83 } 84 } 85 return false; 86 } 87 88 @Override loadClass(String name, boolean resolve)89 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 90 Class<?> clazz = null; 91 92 // Reject forbidden classes to be loaded unexpectedly. 93 if (mRejectedClassesList.contains(name)) { 94 throw new AssertionError("class " + name + " is forbidden!"); 95 } 96 97 // Try loading classes from Android boot classes (Android framework). 98 try { 99 clazz = getSystemClassLoader().loadClass(name); 100 } catch (ClassNotFoundException ignored) { 101 // Non-Android-framework classes would go through the following process, so continue. 102 } 103 104 // Allow allow-listed container classes to be loaded outside plugin's class loader. 105 if (clazz == null 106 && (mContainerClassesAllowlist.contains(name) 107 || startsWithAnyPrefixFromSet(name, mContainerPackagesAllowlist))) { 108 clazz = super.loadClass(name, resolve); 109 } 110 111 // Push back to use plugin's class loader. 112 if (clazz == null) { 113 throw new ClassNotFoundException("Isolation: deferring to plugin's class loader"); 114 } 115 116 return clazz; 117 } 118 } 119