com.android.ide.common.rendering.RenderSecurityManager Maven / Gradle / Ivy
/*
* 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.
*
* 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 sIsRenderThread = new InheritableThreadLocal() {
@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.)
*
* Note: By default a security manager is not active. You need to call
* {@link #setActive(boolean, Object)} with true to activate it, instead 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.(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);
}
}
}
}
}