/* * Copyright (C) 2013 The Android Open Source Project * * Licensed 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 com.android.ide.common.rendering; import static com.android.SdkConstants.DOT_CLASS; import static com.android.SdkConstants.DOT_JAR; import static com.android.SdkConstants.VALUE_FALSE; import com.android.annotations.Nullable; import com.android.utils.ILogger; import java.io.File; import java.io.FileDescriptor; import java.io.FilePermission; import java.io.IOException; import java.lang.reflect.Member; import java.net.InetAddress; import java.security.Permission; import java.util.PropertyPermission; /** * A {@link java.lang.SecurityManager} which is used for layout lib rendering, to * prevent custom views from accidentally exiting the whole IDE if they call * {@code System.exit}, as well as unintentionally writing files etc. * <p> * The security manager only checks calls on the current thread for which it * was made active with a call to {@link #setActive(boolean, Object)}, as well as any * threads constructed from the render thread. */ public class RenderSecurityManager extends SecurityManager { /** Property used to disable sandbox */ public static final String ENABLED_PROPERTY = "android.render.sandbox"; /** Whether we should restrict reading to certain paths */ public static final boolean RESTRICT_READS = false; /** * Whether the security manager is enabled for this session (it might still * be inactive, either because it's active for a different thread, or because * it has been disabled via {@link #setActive(boolean, Object)} (which sets the * per-instance mEnabled flag) */ public static boolean sEnabled = !VALUE_FALSE.equals(System.getProperty(ENABLED_PROPERTY)); /** * Thread local data which indicates whether the current thread is relevant for * this security manager. This is an inheritable thread local such that any threads * spawned from this thread will also apply the security manager; otherwise code * could just create new threads and execute code separate from the security manager * there. */ private static ThreadLocal<Boolean> sIsRenderThread = new InheritableThreadLocal<Boolean>() { @Override protected synchronized Boolean initialValue() { return Boolean.FALSE; } @Override protected synchronized Boolean childValue(Boolean parentValue) { return parentValue; } }; /** Secret which must be provided by callers wishing to deactivate the security manager */ private static Object sCredential; /** For debugging purposes */ private static String sLastFailedPath; private boolean mAllowSetSecurityManager; private boolean mDisabled; @SuppressWarnings("FieldCanBeLocal") private String mSdkPath; @SuppressWarnings("FieldCanBeLocal") private String mProjectPath; private String mTempDir; private String mNormalizedTempDir; private String mCanonicalTempDir; private String mAppTempDir; private SecurityManager myPreviousSecurityManager; private ILogger mLogger; /** * Returns the current render security manager, if any. This will only return * non-null if there is an active {@linkplain RenderSecurityManager} as the * current global security manager. */ @Nullable public static RenderSecurityManager getCurrent() { if (sIsRenderThread.get()) { SecurityManager securityManager = System.getSecurityManager(); if (securityManager instanceof RenderSecurityManager) { RenderSecurityManager manager = (RenderSecurityManager) securityManager; return manager.isRelevant() ? manager : null; } } return null; } /** * Creates a security manager suitable for controlling access to custom views * being rendered by layoutlib, ensuring that they don't accidentally try to * write files etc (which could corrupt data if they for example assume device * paths that are not the same for the running IDE; for example, they could try * to clear out their own local app storage, which in the IDE could be the * user's home directory.) * <p> * Note: By default a security manager is not active. You need to call * {@link #setActive(boolean, Object)} with true to activate it, <b>instead</b> of just calling * {@link System#setSecurityManager(SecurityManager)}. * * @param sdkPath an optional path to the SDK install being used by layoutlib; * this is used to white-list path prefixes for layoutlib resource * lookup * @param projectPath a path to the project directory, used for similar purposes */ public RenderSecurityManager( @Nullable String sdkPath, @Nullable String projectPath) { mSdkPath = sdkPath; mProjectPath = projectPath; mTempDir = System.getProperty("java.io.tmpdir"); mNormalizedTempDir = new File(mTempDir).getPath(); // will call fs.normalize() on the path sLastFailedPath = null; } /** Sets an optional logger. Returns this for constructor chaining. */ public RenderSecurityManager setLogger(@Nullable ILogger logger) { mLogger = logger; return this; } /** Sets an optional application temp directory. Returns this for constructor chaining. */ public RenderSecurityManager setAppTempDir(@Nullable String appTempDir) { mAppTempDir = appTempDir; return this; } /** * Sets whether the {@linkplain RenderSecurityManager} is active or not. * If it is being set as active, the passed in credential is remembered * and anyone wishing to turn off the security manager must provide the * same credential. * * @param active whether to turn on or off the security manager * @param credential when turning off the security manager, the exact same * credential passed in to the earlier activation call */ public void setActive(boolean active, @Nullable Object credential) { SecurityManager current = System.getSecurityManager(); boolean isActive = current == this; if (active == isActive) { return; } if (active) { // Enable assert !(current instanceof RenderSecurityManager); myPreviousSecurityManager = current; sIsRenderThread.set(true); mDisabled = false; System.setSecurityManager(this); //noinspection AssignmentToStaticFieldFromInstanceMethod sCredential = credential; } else { if (credential != sCredential) { throw RenderSecurityException.create("Invalid credential"); } // Disable mAllowSetSecurityManager = true; // Don't set mDisabled and clear sInRenderThread yet: the call // to revert to the previous security manager below will trigger // a check permission, and in that code we need to distinguish between // this call (isRelevant() should return true) and other threads calling // it outside the scope of the security manager try { // Only reset the security manager if it hasn't already been set to // something else. If other threads try to do the same thing we could have // a problem; if they sampled the render security manager while it was globally // active, replaced it with their own, and sometime in the future try to // set it back, it will be active when we didn't intend for it to be. That's // why there is also the {@code mDisabled} flag, used to ignore any requests // later on. if (current instanceof RenderSecurityManager) { System.setSecurityManager(myPreviousSecurityManager); } else if (mLogger != null) { sIsRenderThread.set(false); mLogger.warning("Security manager was changed behind the scenes: ", current); } } finally { mDisabled = true; mAllowSetSecurityManager = false; sIsRenderThread.set(false); } } } private boolean isRelevant() { return sEnabled && !mDisabled && sIsRenderThread.get(); } /** * Disposes the security manager. An alias for calling {@link #setActive} with * false. * * @param credential the sandbox credential initially passed to * {@link #setActive(boolean, Object)} */ public void dispose(@Nullable Object credential) { setActive(false, credential); } /** * Enters a code region where the sandbox is not needed * * @param credential a credential which proves that the caller has the right to do this * @return a token which should be passed back to {@link #exitSafeRegion(boolean)} */ public static boolean enterSafeRegion(@Nullable Object credential) { boolean token = sEnabled; if (credential == sCredential) { sEnabled = false; } return token; } /** * Exits a code region where the sandbox was not needed * * @param token the token which was returned back from the paired * {@link #enterSafeRegion(Object)} call */ public static void exitSafeRegion(boolean token) { sEnabled = token; } /** * Returns the most recently denied path. * * @return the most recently denied path */ @Nullable public static String getLastFailedPath() { return sLastFailedPath; } // Permitted by custom views: access any package or member, read properties @Override public void checkPackageAccess(String pkg) { } @Override public void checkMemberAccess(Class<?> clazz, int which) { if (which == Member.DECLARED && isRelevant() && "com.android.ide.common.rendering.RenderSecurityManager".equals(clazz.getName())) { throw RenderSecurityException.create("Reflection", clazz.getName()); } } @Override public void checkPropertyAccess(String property) { } @Override public void checkLink(String lib) { // Allow linking with relative paths // Needed to for example load the "fontmanager" library from layout lib (from the // BiDiRenderer's layoutGlyphVector call if (isRelevant() && (lib.indexOf('/') != -1 || lib.indexOf('\\') != -1)) { if (lib.startsWith(System.getProperty("java.home"))) { return; // Allow loading JRE libraries } throw RenderSecurityException.create("Link", lib); } } @Override public void checkCreateClassLoader() { // TODO: Layoutlib makes heavy use of this, so we can't block it yet. // To fix this we should make a local class loader, passed to layoutlib, which // knows how to reset the security manager } //------------------------------------------------------------------------------------------ // Reading is permitted for certain files only //------------------------------------------------------------------------------------------ @SuppressWarnings({"PointlessBooleanExpression", "ConstantConditions"}) @Override public void checkRead(String file) { if (RESTRICT_READS && isRelevant() && !isReadingAllowed(file)) { throw RenderSecurityException.create("Read", file); } } @SuppressWarnings({"PointlessBooleanExpression", "ConstantConditions"}) @Override public void checkRead(String file, Object context) { if (RESTRICT_READS && isRelevant() && !isReadingAllowed(file)) { throw RenderSecurityException.create("Read", file); } } private boolean isReadingAllowed(String path) { if (RESTRICT_READS) { // Allow reading files in the SDK install (fonts etc) if (mSdkPath != null && path.startsWith(mSdkPath)) { return true; } // Allowing reading resources in the project, such as icons if (mProjectPath != null && path.startsWith(mProjectPath)) { return true; } if (path.startsWith("#") && path.indexOf(File.separatorChar) == -1) { // It's really layoutlib's ResourceHelper.getColorStateList which calls isFile() // on values to see if it's a file or a color. return true; } // Needed by layoutlib's class loader. Note that we've locked down the ability to // create new class loaders. if (path.endsWith(DOT_CLASS) || path.endsWith(DOT_JAR)) { return true; } // Allow reading files in temp if (isTempDirPath(path)) { return true; } String javaHome = System.getProperty("java.home"); if (path.startsWith(javaHome)) { // Allow JDK to load its own classes return true; } else if (javaHome.endsWith("/Contents/Home")) { // On Mac, Home lives two directory levels down from the real home, and we // sometimes need to read from sibling directories (e.g. ../Libraries/ etc) if (path.regionMatches(0, javaHome, 0, javaHome.length() - "Contents/Home".length())) { return true; } } return false; } else { return true; } } @SuppressWarnings("RedundantIfStatement") private boolean isWritingAllowed(String path) { return isTempDirPath(path); } private boolean isTempDirPath(String path) { if (path.startsWith(mTempDir) || path.startsWith(mNormalizedTempDir)) { return true; } if (mAppTempDir != null && path.startsWith(mAppTempDir)) { return true; } // Work around weird temp directories try { if (mCanonicalTempDir == null) { mCanonicalTempDir = new File(mNormalizedTempDir).getCanonicalPath(); } if (path.startsWith(mCanonicalTempDir) || new File(path).getCanonicalPath().startsWith(mCanonicalTempDir)) { return true; } } catch (IOException e) { // ignore } sLastFailedPath = path; return false; } @SuppressWarnings({"SpellCheckingInspection", "RedundantIfStatement"}) private static boolean isPropertyWriteAllowed(String name) { // Linux sets this on fontmanager load; allow it since even if code points // to their own classes they don't get additional privileges, it's just like // using reflection if (name.equals("sun.font.fontmanager")) { return true; } // Toolkit initializations if (name.startsWith("sun.awt.") || name.startsWith("apple.awt.")) { return true; } if (name.equals("user.timezone")) { return true; } return false; } //------------------------------------------------------------------------------------------ // Not permitted: //------------------------------------------------------------------------------------------ @Override public void checkExit(int status) { // Probably not intentional in a custom view; would take down the whole IDE! if (isRelevant()) { throw RenderSecurityException.create("Exit", String.valueOf(status)); } super.checkExit(status); } @Override public void checkPropertiesAccess() { if (isRelevant()) { throw RenderSecurityException.create("Property", null); } } // Prevent code execution/linking/loading @Override public void checkPackageDefinition(String pkg) { if (isRelevant()) { throw RenderSecurityException.create("Package", pkg); } } @Override public void checkExec(String cmd) { if (isRelevant()) { throw RenderSecurityException.create("Exec", cmd); } } // Prevent network access @Override public void checkConnect(String host, int port) { if (isRelevant()) { throw RenderSecurityException.create("Socket", host + ":" + port); } } @Override public void checkConnect(String host, int port, Object context) { if (isRelevant()) { throw RenderSecurityException.create("Socket", host + ":" + port); } } @Override public void checkListen(int port) { if (isRelevant()) { throw RenderSecurityException.create("Socket", "port " + port); } } @Override public void checkAccept(String host, int port) { if (isRelevant()) { throw RenderSecurityException.create("Socket", host + ":" + port); } } @Override public void checkSetFactory() { if (isRelevant()) { throw RenderSecurityException.create("Socket", null); } } @Override public void checkMulticast(InetAddress inetAddress) { if (isRelevant()) { throw RenderSecurityException.create("Socket", inetAddress.getCanonicalHostName()); } } @SuppressWarnings("deprecation") @Override public void checkMulticast(InetAddress inetAddress, byte ttl) { if (isRelevant()) { throw RenderSecurityException.create("Socket", inetAddress.getCanonicalHostName()); } } // Prevent file access @Override public void checkDelete(String file) { if (isRelevant()) { // Allow writing to temp if (isWritingAllowed(file)) { return; } throw RenderSecurityException.create("Delete", file); } } @Override public void checkAwtEventQueueAccess() { if (isRelevant()) { throw RenderSecurityException.create("Event", null); } } // Prevent writes @Override public void checkWrite(FileDescriptor fileDescriptor) { if (isRelevant()) { throw RenderSecurityException.create("Write", fileDescriptor.toString()); } } @Override public void checkWrite(String file) { if (isRelevant()) { if (isWritingAllowed(file)) { return; } throw RenderSecurityException.create("Write", file); } } // Misc @Override public void checkPrintJobAccess() { if (isRelevant()) { throw RenderSecurityException.create("Print", null); } } @Override public void checkSystemClipboardAccess() { if (isRelevant()) { throw RenderSecurityException.create("Clipboard", null); } } @Override public boolean checkTopLevelWindow(Object context) { if (isRelevant()) { throw RenderSecurityException.create("Window", null); } return false; } @Override public void checkAccess(Thread thread) { // Turns out layoutlib sometimes creates asynchronous calls, for example // java.lang.Thread.<init>(Thread.java:521) // at android.os.AsyncTask$1.newThread(AsyncTask.java:189) // at java.util.concurrent.ThreadPoolExecutor.addThread(ThreadPoolExecutor.java:670) // at java.util.concurrent.ThreadPoolExecutor.addIfUnderCorePoolSize(ThreadPoolExecutor.java:706) // at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:650) // at android.os.AsyncTask$SerialExecutor.scheduleNext(AsyncTask.java:244) // at android.os.AsyncTask$SerialExecutor.execute(AsyncTask.java:238) // at android.os.AsyncTask.execute(AsyncTask.java:604) // at android.widget.TextView.updateTextServicesLocaleAsync(TextView.java:8078) // This may not work correctly for render sessions, which are treated as synchronous // by callers. We should re-enable these checks to chase down these calls and // eliminate them from layoutlib, but until we do, it's necessary to allow thread // creation. } @Override public void checkAccess(ThreadGroup threadGroup) { // See checkAccess(Thread) } @Override public void checkPermission(Permission permission) { String name = permission.getName(); if ("setSecurityManager".equals(name)) { if (isRelevant()) { if (!mAllowSetSecurityManager) { throw RenderSecurityException.create("Security", null); } } else if (mLogger != null) { mLogger.warning("RenderSecurityManager being replaced by another thread"); } } else if (isRelevant()) { String actions = permission.getActions(); //noinspection PointlessBooleanExpression,ConstantConditions if (RESTRICT_READS && "read".equals(actions)) { if (!isReadingAllowed(name)) { throw RenderSecurityException.create("Read", name); } } else if (!actions.isEmpty() && !actions.equals("read")) { // write, execute, delete, readlink if (!(permission instanceof FilePermission) || !isWritingAllowed(name)) { if (permission instanceof PropertyPermission && isPropertyWriteAllowed(name)) { return; } throw RenderSecurityException.create("Write", name); } } } } }