Java tutorial
/* * Copyright (C) 2017 The Regents of the University of Michigan * * 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 edu.umich.flowfence.service; import android.util.Log; import org.apache.commons.lang3.ObjectUtils; import java.util.ArrayDeque; import java.util.BitSet; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.Map; import java.util.Objects; import edu.umich.flowfence.common.FlowfenceConstants; import edu.umich.flowfence.common.TaintSet; public class SandboxManager { private static final String TAG = "SandboxManager"; private static final boolean localLOGV = Log.isLoggable(TAG, Log.VERBOSE); private static final boolean localLOGD = Log.isLoggable(TAG, Log.DEBUG); private static final int SANDBOX_COUNT = FlowfenceConstants.NUM_SANDBOXES; private static final float COST_START_PROCESS = 50.0f; private static final float COST_LOAD_CODE = 10.0f; private static final float COST_RESOLVE_QM = 3.0f; private static final float COST_MARSHAL_OUT = 1.0f; private static final float COST_MARSHAL_IN = 1.0f; private final LinkedHashMap<Sandbox, ObjectUtils.Null> mIdleSandboxes = new LinkedHashMap<>(SANDBOX_COUNT * 2, 0.75f, true); private final ArrayDeque<Sandbox> mStoppedSandboxes = new ArrayDeque<>(SANDBOX_COUNT); private final HashSet<Sandbox> mRunningSandboxes = new HashSet<>(SANDBOX_COUNT * 2); private final ArrayDeque<Sandbox> mHotSpares = new ArrayDeque<>(SANDBOX_COUNT); private final LinkedList<AsyncCallback> mPendingCallbacks = new LinkedList<>(); private final Object mExecutionReferenceKey = new Object(); private int mMaxCount = 0; private int mMaxIdleCount = SANDBOX_COUNT; private int mMinHotSpares = 1; private int mMaxHotSpares = SANDBOX_COUNT; public SandboxManager() { } public interface AsyncCallback { Sandbox tryFindSandbox(SandboxManager manager); void execute(Sandbox finalChoice); } private static abstract class FutureCallback implements AsyncCallback { private volatile boolean mComplete = false; private Sandbox mSandbox = null; @Override public void execute(Sandbox sb) { synchronized (this) { boolean wasComplete = mComplete; mComplete = true; if (!wasComplete) { mSandbox = sb; this.notifyAll(); } } } public Sandbox await() { synchronized (this) { while (!mComplete) { try { this.wait(); } catch (InterruptedException e) { } } return mSandbox; } } } private static void dumpSandbox(Sandbox sb, BitSet seen) { int id = sb.getID(); String result = String.format(" %s %s package=%s", sb, sb.getTaints(), sb.getAssignedPackage()); if (seen == null) { Log.w(TAG, result + " [LEAKED]"); } else if (seen.get(id)) { Log.w(TAG, result + " [DUPLICATE]"); } else { Log.v(TAG, result); seen.set(id); } } private void dumpSandboxes() { if (localLOGV) { BitSet seenSandboxes = new BitSet(SANDBOX_COUNT); Log.v(TAG, ">>> Dumping current sandbox state:"); Log.v(TAG, "Running: " + mRunningSandboxes.size() + " sandboxes"); for (Sandbox sb : mRunningSandboxes) { dumpSandbox(sb, seenSandboxes); } Log.v(TAG, "Idle: " + mIdleSandboxes.size() + " sandboxes (LRU order)"); for (Sandbox sb : mIdleSandboxes.keySet()) { dumpSandbox(sb, seenSandboxes); } Log.v(TAG, "Stopped: " + mStoppedSandboxes.size() + " sandboxes"); for (Sandbox sb : mStoppedSandboxes) { dumpSandbox(sb, seenSandboxes); } Log.v(TAG, "Hot spares: " + mHotSpares.size() + " sandboxes"); for (Sandbox sb : mHotSpares) { dumpSandbox(sb, seenSandboxes); } seenSandboxes.flip(0, SANDBOX_COUNT); // true = unseen if (!seenSandboxes.isEmpty()) { Log.w(TAG, "WARNING: leaked " + seenSandboxes.cardinality() + " sandboxes"); int leaked = -1; while ((leaked = seenSandboxes.nextSetBit(leaked + 1)) >= 0) { dumpSandbox(Sandbox.get(leaked), null); } } else { Log.v(TAG, "No leaks detected"); } Log.v(TAG, "<<< End of state dump"); } } private synchronized Map<AsyncCallback, Sandbox> tryExecuteQueueLocked() { Map<AsyncCallback, Sandbox> rv = null; Iterator<AsyncCallback> callbackIter = mPendingCallbacks.iterator(); while (callbackIter.hasNext()) { AsyncCallback callback = callbackIter.next(); Sandbox candidate = tryBeginExecution(callback.tryFindSandbox(this)); if (candidate != null) { if (rv == null) { rv = new LinkedHashMap<>(); } callbackIter.remove(); rv.put(callback, candidate); } } return rv; } private void tryExecuteQueueUnlocked(Map<AsyncCallback, Sandbox> toExecute) { if (toExecute != null) { for (Map.Entry<AsyncCallback, Sandbox> entry : toExecute.entrySet()) { entry.getKey().execute(entry.getValue()); } } } private void tryExecuteQueue() { Map<AsyncCallback, Sandbox> map; synchronized (this) { map = tryExecuteQueueLocked(); } tryExecuteQueueUnlocked(map); } public int setMaxSandboxCount(int count) { Map<AsyncCallback, Sandbox> callbacks = null; boolean shouldRunQueue = false; int oldCount; synchronized (this) { oldCount = mMaxCount; int newCount = Math.max(0, Math.min(count, SANDBOX_COUNT)); Log.i(TAG, String.format("Changing sandbox count from %d to %d", oldCount, newCount)); dumpSandboxes(); if (newCount < oldCount) { // Taking sandboxes away. for (int i = oldCount - 1; i >= newCount; i--) { Sandbox sb = Sandbox.get(i); if (mStoppedSandboxes.remove(sb)) { // Was previously stopped; do nothing. } else if (mIdleSandboxes.remove(sb) != null) { // Was previously idle; stop. sb.stop(this); } else if (mHotSpares.remove(sb)) { // Was previously hot spare; stop and replace hot spare. sb.stop(this); } else if (mRunningSandboxes.remove(sb)) { // Was previously running; let it keep running, putSandbox() will ignore // this sandbox when the time comes. } } } else if (newCount > oldCount) { // Adding sandboxes. for (int i = oldCount; i < newCount; i++) { Sandbox sb = Sandbox.get(i); if (!mRunningSandboxes.contains(sb)) { mStoppedSandboxes.add(sb); } } // Wake up people waiting for a new hot spare. shouldRunQueue = true; } mMaxCount = newCount; refillHotSpares(); if (shouldRunQueue) { callbacks = tryExecuteQueueLocked(); } dumpSandboxes(); } if (shouldRunQueue) { tryExecuteQueueUnlocked(callbacks); } return oldCount; } public synchronized int setMaxIdleSandboxCount(int count) { int oldCount = mMaxIdleCount; int newCount = Math.max(0, Math.min(count, SANDBOX_COUNT)); Log.i(TAG, "Changing max idle count from " + oldCount + " to " + newCount); dumpSandboxes(); mMaxIdleCount = newCount; trimIdle(); dumpSandboxes(); return oldCount; } public synchronized int setMinHotSpare(int count) { int oldCount = mMinHotSpares; int newCount = Math.max(0, Math.min(count, mMaxHotSpares)); Log.i(TAG, "Changing min hot spares from " + oldCount + " to " + newCount); dumpSandboxes(); mMinHotSpares = newCount; if (mHotSpares.size() < mMinHotSpares) { refillHotSpares(); } return oldCount; } public synchronized int setMaxHotSpare(int count) { int oldCount = mMaxHotSpares; int newCount = Math.max(mMinHotSpares, Math.min(count, SANDBOX_COUNT)); Log.i(TAG, "Changing max hot spares from " + oldCount + " to " + newCount); dumpSandboxes(); mMaxHotSpares = newCount; while (mHotSpares.size() > mMaxHotSpares) { mIdleSandboxes.put(mHotSpares.remove(), ObjectUtils.NULL); } trimIdle(); return oldCount; } public synchronized void start() { setMaxSandboxCount(SANDBOX_COUNT); } public synchronized void stop() { setMaxSandboxCount(0); } private Sandbox evictIdle() { Map.Entry<Sandbox, ?> eldest = mIdleSandboxes.eldest(); Sandbox victim = eldest.getKey(); mIdleSandboxes.remove(victim); return victim; } private void trimIdle() { while (mIdleSandboxes.size() > mMaxIdleCount) { Sandbox victim = evictIdle(); if (mHotSpares.size() < mMaxHotSpares) { victim.restart(); mHotSpares.add(victim); } else { victim.stop(this); mStoppedSandboxes.add(victim); } } } private void refillHotSpares() { while (mHotSpares.size() < mMinHotSpares && addHotSpare()) { dumpSandboxes(); } } private boolean addHotSpare() { Sandbox newHotSpare = null; // Now replace the hot spare. if (!mStoppedSandboxes.isEmpty()) { newHotSpare = mStoppedSandboxes.poll(); if (localLOGD) { Log.d(TAG, "Use stopped sandbox " + newHotSpare); } newHotSpare.start(this); } else if (!mIdleSandboxes.isEmpty()) { newHotSpare = evictIdle(); if (localLOGD) { Log.d(TAG, "Use idle sandbox " + newHotSpare); } newHotSpare.restart(); } else { Log.w(TAG, "No more hot spare candidates available"); } if (newHotSpare != null) { mHotSpares.add(newHotSpare); return true; } else { return false; } } private synchronized Sandbox tryGetHotSpare() { Sandbox result; while ((result = mHotSpares.peek()) == null) { if (addHotSpare()) { continue; } return null; } if (localLOGD) { Log.d(TAG, "Using hot spare " + result); } refillHotSpares(); return result; } private synchronized Sandbox tryBeginExecution(Sandbox sb) { if (sb == null) { return null; } if (mHotSpares.peek() == sb) { mHotSpares.remove(); } else if (mIdleSandboxes.remove(sb) == null) { Log.e(TAG, "Sandbox not idle when we're trying to execute in it"); } mRunningSandboxes.add(sb); sb.start(mExecutionReferenceKey); return sb; } private synchronized Sandbox tryGetSandboxForResolve(String packageName) { Sandbox cheapestSandbox = null; if (localLOGD) { Log.d(TAG, "getSandboxForResolve " + packageName); dumpSandboxes(); } // Start out with the hot spare. float cheapestCost = !mHotSpares.isEmpty() ? COST_LOAD_CODE + 0.01f : Float.POSITIVE_INFINITY; // Is there an idle sandbox that's cheaper than the hot spare? for (Sandbox sb : mIdleSandboxes.keySet()) { String assignedPackage = sb.getAssignedPackage(); if (assignedPackage != null && !assignedPackage.equals(packageName)) { continue; } float cost = 0.0f; if (!sb.hasLoadedPackage(packageName)) { // Need to load this package into this sandbox. cost += COST_LOAD_CODE; } if (localLOGD) { Log.d(TAG, String.format("%s: cost %g", sb, cost)); } // We don't care about taints or anything like that. if (cost < cheapestCost) { cheapestCost = cost; cheapestSandbox = sb; } } if (localLOGD) { Log.d(TAG, String.format("Final choice: %s, cost %g", (cheapestSandbox == null) ? "hot spare" : cheapestSandbox, cheapestCost)); } return (cheapestSandbox != null) ? cheapestSandbox : tryGetHotSpare(); } public synchronized Sandbox tryGetSandboxForCall(CallRecord record) { Sandbox cheapestSandbox = null; if (localLOGD) { Log.d(TAG, "getSandboxForCall " + record); dumpSandboxes(); } int numHandles = record.getPredecessors().size(); int numUnmarshalledHandles = 0; for (Handle h : record.getPredecessors()) { if (!h.isMarshalled()) { numUnmarshalledHandles++; } } // Start out with the hot spare. float cheapestCost = !mHotSpares.isEmpty() ? COST_LOAD_CODE + COST_RESOLVE_QM + 0.01f + COST_MARSHAL_OUT * numUnmarshalledHandles + COST_MARSHAL_IN * numHandles : Float.POSITIVE_INFINITY; if (localLOGD) { Log.d(TAG, String.format("%d handles, %d unmarshalled", numHandles, numUnmarshalledHandles)); Log.d(TAG, String.format("Hot spare cost: %g", cheapestCost)); } // Can we get away with an idle sandbox? TaintSet inboundTaint = record.getInboundTaints(); if (localLOGD) { Log.d(TAG, String.format("Inbound taint: %s", inboundTaint)); } final String packageName = record.getQM().getDescriptor().definingClass.getPackageName(); for (Sandbox sb : mIdleSandboxes.keySet()) { String assignedPackage = sb.getAssignedPackage(); if (assignedPackage != null && !assignedPackage.equals(packageName)) { continue; } TaintSet sandboxTaint = sb.getTaints(); boolean mustMarshalOut = false; if (!sandboxTaint.isSubsetOf(inboundTaint)) { // SB more tainted than inbound; skip this SB continue; } else if (!inboundTaint.isSubsetOf(sandboxTaint)) { // Inbound more tainted than SB; must marshal things out of SB mustMarshalOut = true; } float cost = 0.0f; if (!sb.hasLoadedPackage(packageName)) { // Need to load this package into this sandbox. cost += COST_LOAD_CODE; } if (!record.getQM().isResolvedIn(sb)) { // Need to resolve the QM in this sandbox. cost += COST_RESOLVE_QM; } if (mustMarshalOut) { cost += COST_MARSHAL_OUT * sb.countUnmarshalledObjects(); } for (Handle h : record.getPredecessors()) { if (!h.isLiveIn(sb)) { cost += COST_MARSHAL_IN; if (!h.isMarshalled()) { cost += COST_MARSHAL_OUT; } } else if (h.getRefCount() == 1) { // We won't need to marshal this one out after all. cost -= COST_MARSHAL_OUT; } } if (localLOGD) { Log.d(TAG, String.format("%s %s: cost %g", sb, sandboxTaint, cost)); } if (cost < cheapestCost) { cheapestCost = cost; cheapestSandbox = sb; } } if (localLOGD) { Log.d(TAG, String.format("Final choice: %s, cost %g", (cheapestSandbox == null) ? "hot spare" : cheapestSandbox, cheapestCost)); } return (cheapestSandbox != null) ? cheapestSandbox : tryGetHotSpare(); } public synchronized Sandbox tryGetSandboxById(final int id, final CallRecord record) { final Sandbox sb = Sandbox.get(id); if (localLOGD) { Log.d(TAG, "getSandboxById " + sb); } if (mRunningSandboxes.contains(sb)) { Log.i(TAG, "Explicitly requested sandbox " + sb + " busy"); return null; } if (mStoppedSandboxes.remove(sb)) { sb.start(this); } if (mHotSpares.remove(sb)) { refillHotSpares(); } // Put into mIdleSandboxes temporarily, beginExecution expects it. mIdleSandboxes.put(sb, ObjectUtils.NULL); if (record != null) { String packageName = record.getQM().getDescriptor().definingClass.getPackageName(); String assignedName = sb.getAssignedPackage(); if ((assignedName != null && !assignedName.equals(packageName)) || !record.getInboundTaints().isSubsetOf(sb.getTaints())) { // We need to restart this sandbox for security reasons. Log.i(TAG, "Restarting " + sb + " for explicit request"); sb.restart(); } } return sb; } public void getSandboxAsync(AsyncCallback callback) { Map<AsyncCallback, Sandbox> callbacks; synchronized (this) { Objects.requireNonNull(callback); mPendingCallbacks.add(callback); callbacks = tryExecuteQueueLocked(); } tryExecuteQueueUnlocked(callbacks); } public Sandbox getSandboxForResolve(final String packageName) { FutureCallback cb = new FutureCallback() { @Override public Sandbox tryFindSandbox(SandboxManager manager) { return manager.tryGetSandboxForResolve(packageName); } }; getSandboxAsync(cb); return cb.await(); } public Sandbox getSandboxById(final int id, final CallRecord record) { FutureCallback cb = new FutureCallback() { @Override public Sandbox tryFindSandbox(SandboxManager manager) { return manager.tryGetSandboxById(id, record); } }; getSandboxAsync(cb); return cb.await(); } public void putSandbox(Sandbox sb) { Map<AsyncCallback, Sandbox> callbacks; synchronized (this) { if (!mRunningSandboxes.remove(sb)) { Log.w(TAG, "Put non-running sandbox " + sb); return; } sb.stop(mExecutionReferenceKey); if (localLOGD) { Log.d(TAG, "putSandbox " + sb); } // This was a trimmed sandbox. Let it go out of circulation. if (sb.getID() >= mMaxCount) { sb.stop(this); return; } mIdleSandboxes.put(sb, ObjectUtils.NULL); refillHotSpares(); trimIdle(); callbacks = tryExecuteQueueLocked(); } tryExecuteQueueUnlocked(callbacks); } public synchronized void onPackageRemoved(String packageName) { for (Sandbox sb : mIdleSandboxes.keySet()) { if (sb.hasLoadedPackage(packageName)) { sb.restart(); } } } }