diff options
20 files changed, 1105 insertions, 95 deletions
diff --git a/api/system-current.txt b/api/system-current.txt index 202d51ff9d92..5463cbab4e20 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -130,6 +130,7 @@ package android { field public static final String NETWORK_FACTORY = "android.permission.NETWORK_FACTORY"; field public static final String NETWORK_MANAGED_PROVISIONING = "android.permission.NETWORK_MANAGED_PROVISIONING"; field public static final String NETWORK_SCAN = "android.permission.NETWORK_SCAN"; + field public static final String NETWORK_SETTINGS = "android.permission.NETWORK_SETTINGS"; field public static final String NETWORK_SETUP_WIZARD = "android.permission.NETWORK_SETUP_WIZARD"; field public static final String NETWORK_SIGNAL_STRENGTH_WAKEUP = "android.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP"; field public static final String NETWORK_STACK = "android.permission.NETWORK_STACK"; @@ -4405,13 +4406,13 @@ package android.net { public class ConnectivityManager { method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createNattKeepalive(@NonNull android.net.Network, @NonNull android.os.ParcelFileDescriptor, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback); method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull java.net.Socket, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback); - method @Deprecated @RequiresPermission("android.permission.NETWORK_SETTINGS") public String getCaptivePortalServerUrl(); + method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public String getCaptivePortalServerUrl(); method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void getLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEntitlementResultListener); method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public boolean isTetheringSupported(); method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public int registerNetworkProvider(@NonNull android.net.NetworkProvider); method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEventCallback); method @Deprecated public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int, int, @NonNull android.os.Handler); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void setAirplaneMode(boolean); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void setAirplaneMode(boolean); method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public boolean shouldAvoidBadWifi(); method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void startCaptivePortalApp(@NonNull android.net.Network, @NonNull android.os.Bundle); method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback); @@ -5784,13 +5785,13 @@ package android.net.wifi { public class WifiManager { method @RequiresPermission("android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE") public void addOnWifiUsabilityStatsListener(@NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.WifiManager.OnWifiUsabilityStatsListener); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void connect(@NonNull android.net.wifi.WifiConfiguration, @Nullable android.net.wifi.WifiManager.ActionListener); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void connect(int, @Nullable android.net.wifi.WifiManager.ActionListener); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void disable(int, @Nullable android.net.wifi.WifiManager.ActionListener); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void forget(int, @Nullable android.net.wifi.WifiManager.ActionListener); - method @NonNull @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public java.util.List<android.util.Pair<android.net.wifi.WifiConfiguration,java.util.Map<java.lang.Integer,java.util.List<android.net.wifi.ScanResult>>>> getAllMatchingWifiConfigs(@NonNull java.util.List<android.net.wifi.ScanResult>); - method @NonNull @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public java.util.Map<android.net.wifi.hotspot2.OsuProvider,java.util.List<android.net.wifi.ScanResult>> getMatchingOsuProviders(@Nullable java.util.List<android.net.wifi.ScanResult>); - method @NonNull @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public java.util.Map<android.net.wifi.hotspot2.OsuProvider,android.net.wifi.hotspot2.PasspointConfiguration> getMatchingPasspointConfigsForOsuProviders(@NonNull java.util.Set<android.net.wifi.hotspot2.OsuProvider>); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void connect(@NonNull android.net.wifi.WifiConfiguration, @Nullable android.net.wifi.WifiManager.ActionListener); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void connect(int, @Nullable android.net.wifi.WifiManager.ActionListener); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void disable(int, @Nullable android.net.wifi.WifiManager.ActionListener); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void forget(int, @Nullable android.net.wifi.WifiManager.ActionListener); + method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public java.util.List<android.util.Pair<android.net.wifi.WifiConfiguration,java.util.Map<java.lang.Integer,java.util.List<android.net.wifi.ScanResult>>>> getAllMatchingWifiConfigs(@NonNull java.util.List<android.net.wifi.ScanResult>); + method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public java.util.Map<android.net.wifi.hotspot2.OsuProvider,java.util.List<android.net.wifi.ScanResult>> getMatchingOsuProviders(@Nullable java.util.List<android.net.wifi.ScanResult>); + method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public java.util.Map<android.net.wifi.hotspot2.OsuProvider,android.net.wifi.hotspot2.PasspointConfiguration> getMatchingPasspointConfigsForOsuProviders(@NonNull java.util.Set<android.net.wifi.hotspot2.OsuProvider>); method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_WIFI_STATE, android.Manifest.permission.READ_WIFI_CREDENTIAL}) public java.util.List<android.net.wifi.WifiConfiguration> getPrivilegedConfiguredNetworks(); method @RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE) public android.net.wifi.WifiConfiguration getWifiApConfiguration(); method @RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE) public int getWifiApState(); @@ -5798,17 +5799,17 @@ package android.net.wifi { method public boolean isPortableHotspotSupported(); method @RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE) public boolean isWifiApEnabled(); method public boolean isWifiScannerSupported(); - method @RequiresPermission("android.permission.NETWORK_SETTINGS") public void registerSoftApCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.WifiManager.SoftApCallback); + method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void registerSoftApCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.WifiManager.SoftApCallback); method @RequiresPermission("android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE") public void removeOnWifiUsabilityStatsListener(@NonNull android.net.wifi.WifiManager.OnWifiUsabilityStatsListener); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void save(@NonNull android.net.wifi.WifiConfiguration, @Nullable android.net.wifi.WifiManager.ActionListener); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void save(@NonNull android.net.wifi.WifiConfiguration, @Nullable android.net.wifi.WifiManager.ActionListener); method @RequiresPermission("android.permission.WIFI_SET_DEVICE_MOBILITY_STATE") public void setDeviceMobilityState(int); method @RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE) public boolean setWifiApConfiguration(android.net.wifi.WifiConfiguration); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startEasyConnectAsConfiguratorInitiator(@NonNull String, int, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.EasyConnectStatusCallback); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startEasyConnectAsEnrolleeInitiator(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.EasyConnectStatusCallback); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startEasyConnectAsConfiguratorInitiator(@NonNull String, int, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.EasyConnectStatusCallback); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startEasyConnectAsEnrolleeInitiator(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.EasyConnectStatusCallback); method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public boolean startScan(android.os.WorkSource); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public boolean startSoftAp(@Nullable android.net.wifi.WifiConfiguration); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startSubscriptionProvisioning(@NonNull android.net.wifi.hotspot2.OsuProvider, @NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.hotspot2.ProvisioningCallback); - method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void stopEasyConnectSession(); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startSubscriptionProvisioning(@NonNull android.net.wifi.hotspot2.OsuProvider, @NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.hotspot2.ProvisioningCallback); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void stopEasyConnectSession(); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public boolean stopSoftAp(); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void updateInterfaceIpState(@Nullable String, int); method @RequiresPermission("android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE") public void updateWifiUsabilityScore(int, int, int); diff --git a/api/test-current.txt b/api/test-current.txt index b3e80a98a2e2..d0c13724b363 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -14,6 +14,7 @@ package android { field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES"; field public static final String MANAGE_ACTIVITY_STACKS = "android.permission.MANAGE_ACTIVITY_STACKS"; field public static final String MANAGE_ROLLBACKS = "android.permission.MANAGE_ROLLBACKS"; + field public static final String NETWORK_SETTINGS = "android.permission.NETWORK_SETTINGS"; field public static final String NETWORK_STACK = "android.permission.NETWORK_STACK"; field public static final String READ_CELL_BROADCASTS = "android.permission.READ_CELL_BROADCASTS"; field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS"; diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java new file mode 100644 index 000000000000..844e72ecf07c --- /dev/null +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2019 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 android.app; +import android.annotation.NonNull; +import android.os.SystemProperties; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +/** + * LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing, + * but doesn't hold a lock across data fetches on query misses. + * + * The intended use case is caching frequently-read, seldom-changed information normally + * retrieved across interprocess communication. Imagine that you've written a user birthday + * information daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface + * over binder. That binder interface looks something like this: + * + * <pre> + * parcelable Birthday { + * int month; + * int day; + * } + * interface IUserBirthdayService { + * Birthday getUserBirthday(int userId); + * } + * </pre> + * + * Suppose the service implementation itself looks like this... + * + * <pre> + * public class UserBirthdayServiceImpl implements IUserBirthdayService { + * private final HashMap<Integer, Birthday> mUidToBirthday; + * @Override + * public synchronized Birthday getUserBirthday(int userId) { + * return mUidToBirthday.get(userId); + * } + * private synchronized void updateBirthdays(Map<Integer, Birthday> uidToBirthday) { + * mUidToBirthday.clear(); + * mUidToBirthday.putAll(uidToBirthday); + * } + * } + * </pre> + * + * ... and we have a client in frameworks (loaded into every app process) that looks + * like this: + * + * <pre> + * public class ActivityThread { + * ... + * public Birthday getUserBirthday(int userId) { + * return GetService("birthdayd").getUserBirthday(userId); + * } + * ... + * } + * </pre> + * + * With this code, every time an app calls {@code getUserBirthday(uid)}, we make a binder call + * to the birthdayd process and consult its database of birthdays. If we query user birthdays + * frequently, we do a lot of work that we don't have to do, since user birthdays + * change infrequently. + * + * PropertyInvalidatedCache is part of a pattern for optimizing this kind of + * information-querying code. Using {@code PropertyInvalidatedCache}, you'd write the client + * this way: + * + * <pre> + * public class ActivityThread { + * ... + * private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache + * private static final String BDAY_CACHE_KEY = "cache_key.birthdayd"; + * private final PropertyInvalidatedCache<Integer, Birthday> mBirthdayCache = new + * PropertyInvalidatedCache<Integer, Birthday>(BDAY_CACHE_MAX, BDAY_CACHE_KEY) { + * @Override + * protected Birthday recompute(Integer userId) { + * return GetService("birthdayd").getUserBirthday(userId); + * } + * }; + * public void disableUserBirthdayCache() { + * mBirthdayCache.disableLocal(); + * } + * public void invalidateUserBirthdayCache() { + * mBirthdayCache.invalidateCache(); + * } + * public Birthday getUserBirthday(int userId) { + * return mBirthdayCache.query(userId); + * } + * ... + * } + * </pre> + * + * With this cache, clients perform a binder call to birthdayd if asking for a user's birthday + * for the first time; on subsequent queries, we return the already-known Birthday object. + * + * User birthdays do occasionally change, so we have to modify the server to invalidate this + * cache when necessary. That invalidation code looks like this: + * + * <pre> + * public class UserBirthdayServiceImpl { + * ... + * public UserBirthdayServiceImpl() { + * ... + * ActivityThread.currentActivityThread().disableUserBirthdayCache(); + * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); + * } + * + * private synchronized void updateBirthdays(Map<Integer, Birthday> uidToBirthday) { + * mUidToBirthday.clear(); + * mUidToBirthday.putAll(uidToBirthday); + * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); + * } + * ... + * } + * </pre> + * + * The call to {@code PropertyInvalidatedCache.invalidateCache()} guarantees that all clients + * will re-fetch birthdays from binder during consequent calls to + * {@code ActivityThread.getUserBirthday()}. Because the invalidate call happens with the lock + * held, we maintain consistency between different client views of the birthday state. The use + * of PropertyInvalidatedCache in this idiomatic way introduces no new race conditions. + * + * PropertyInvalidatedCache has a few other features for doing things like incremental + * enhancement of cached values and invalidation of multiple caches (that all share the same + * property key) at once. + * + * {@code BDAY_CACHE_KEY} is the name of a property that we set to an opaque unique value each + * time we update the cache. SELinux configuration must allow everyone to read this property + * and it must allow any process that needs to invalidate the cache (here, birthdayd) to write + * the property. (These properties conventionally begin with the "cache_key." prefix.) + * + * The {@code UserBirthdayServiceImpl} constructor calls {@code disableUserBirthdayCache()} so + * that calls to {@code getUserBirthday} from inside birthdayd don't go through the cache. In + * this local case, there's no IPC, so use of the cache is (depending on exact + * circumstance) unnecessary. + * + * @param <Query> The class used to index cache entries: must be hashable and comparable + * @param <Result> The class holding cache entries; use a boxed primitive if possible + * + * {@hide} + */ +public abstract class PropertyInvalidatedCache<Query, Result> { + private static final long NONCE_UNSET = 0; + private static final long NONCE_DISABLED = -1; + + private static final String TAG = "PropertyInvalidatedCache"; + private static final boolean DEBUG = false; + private static final boolean ENABLE = true; + + private final Object mLock = new Object(); + + /** + * Name of the property that holds the unique value that we use to invalidate the cache. + */ + private final String mPropertyName; + + /** + * Handle to the {@code mPropertyName} property, transitioning to non-{@code null} once the + * property exists on the system. + */ + private volatile SystemProperties.Handle mPropertyHandle; + + @GuardedBy("mLock") + private final LinkedHashMap<Query, Result> mCache; + + /** + * The last value of the {@code mPropertyHandle} that we observed. + */ + @GuardedBy("mLock") + private long mLastSeenNonce = NONCE_UNSET; + + /** + * Whether we've disabled the cache in this process. + */ + private boolean mDisabled = false; + + /** + * Make a new property invalidated cache. + * + * @param maxEntries Maximum number of entries to cache; LRU discard + * @param propertyName Name of the system property holding the cache invalidation nonce + */ + public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName) { + mPropertyName = propertyName; + mCache = new LinkedHashMap<Query, Result>( + 2 /* start small */, + 0.75f /* default load factor */, + true /* LRU access order */) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxEntries; + } + }; + } + + /** + * Forget all cached values. + */ + public final void clear() { + synchronized (mLock) { + mCache.clear(); + } + } + + /** + * Fetch a result from scratch in case it's not in the cache at all. Called unlocked: may + * block. If this function returns null, the result of the cache query is null. There is no + * "negative cache" in the query: we don't cache null results at all. + */ + protected abstract Result recompute(Query query); + + /** + * Make result up-to-date on a cache hit. Called unlocked; + * may block. + * + * Return either 1) oldResult itself (the same object, by reference equality), in which + * case we just return oldResult as the result of the cache query, 2) a new object, which + * replaces oldResult in the cache and which we return as the result of the cache query + * after performing another property read to make sure that the result hasn't changed in + * the meantime (if the nonce has changed in the meantime, we drop the cache and try the + * whole query again), or 3) null, which causes the old value to be removed from the cache + * and null to be returned as the result of the cache query. + */ + protected Result refresh(Result oldResult, Query query) { + return oldResult; + } + + private long getCurrentNonce() { + SystemProperties.Handle handle = mPropertyHandle; + if (handle == null) { + handle = SystemProperties.find(mPropertyName); + if (handle == null) { + return NONCE_UNSET; + } + mPropertyHandle = handle; + } + return handle.getLong(NONCE_UNSET); + } + + /** + * Disable the use of this cache in this process. + */ + public final void disableLocal() { + synchronized (mLock) { + mDisabled = true; + mCache.clear(); + } + } + + /** + * Return whether the cache is disabled in this process. + */ + public final boolean isDisabledLocal() { + return mDisabled; + } + + /** + * Get a value from the cache or recompute it. + */ + public Result query(Query query) { + // Let access to mDisabled race: it's atomic anyway. + long currentNonce = (ENABLE && !mDisabled) ? getCurrentNonce() : NONCE_DISABLED; + for (;;) { + if (currentNonce == NONCE_DISABLED || currentNonce == NONCE_UNSET) { + if (DEBUG) { + Log.d(TAG, + String.format("cache %s for %s", + currentNonce == NONCE_DISABLED ? "disabled" : "unset", + query)); + } + return recompute(query); + } + final Result cachedResult; + synchronized (mLock) { + if (currentNonce == mLastSeenNonce) { + cachedResult = mCache.get(query); + } else { + if (DEBUG) { + Log.d(TAG, + String.format("clearing cache because nonce changed [%s] -> [%s]", + mLastSeenNonce, currentNonce)); + } + mCache.clear(); + mLastSeenNonce = currentNonce; + cachedResult = null; + } + } + // Cache hit --- but we're not quite done yet. A value in the cache might need to + // be augmented in a "refresh" operation. The refresh operation can combine the + // old and the new nonce values. In order to make sure the new parts of the value + // are consistent with the old, possibly-reused parts, we check the property value + // again after the refresh and do the whole fetch again if the property invalidated + // us while we were refreshing. + if (cachedResult != null) { + final Result refreshedResult = refresh(cachedResult, query); + if (refreshedResult != cachedResult) { + if (DEBUG) { + Log.d(TAG, "cache refresh for " + query); + } + final long afterRefreshNonce = getCurrentNonce(); + if (currentNonce != afterRefreshNonce) { + currentNonce = afterRefreshNonce; + if (DEBUG) { + Log.d(TAG, "restarting query because nonce changed in refresh"); + } + continue; + } + synchronized (mLock) { + if (currentNonce != mLastSeenNonce) { + // Do nothing: cache is already out of date. Just return the value + // we already have: there's no guarantee that the contents of mCache + // won't become invalid as soon as we return. + } else if (refreshedResult == null) { + mCache.remove(query); + } else { + mCache.put(query, refreshedResult); + } + } + return refreshedResult; + } + if (DEBUG) { + Log.d(TAG, "cache hit for " + query); + } + return cachedResult; + } + // Cache miss: make the value from scratch. + if (DEBUG) { + Log.d(TAG, "cache miss for " + query); + } + final Result result = recompute(query); + synchronized (mLock) { + // If someone else invalidated the cache while we did the recomputation, don't + // update the cache with a potentially stale result. + if (mLastSeenNonce == currentNonce && result != null) { + mCache.put(query, result); + } + } + return result; + } + } + + // Inner class avoids initialization in processes that don't do any invalidation + private static final class NoPreloadHolder { + private static final AtomicLong sNextNonce = new AtomicLong((new Random()).nextLong()); + public static long next() { + return sNextNonce.getAndIncrement(); + } + } + + /** + * Non-static convenience version of disableSystemWide() for situations in which only a + * single PropertyInvalidatedCache is keyed on a particular property value. + * + * When multiple caches share a single property value, using an instance method on one of + * the cache objects to invalidate all of the cache objects becomes confusing and you should + * just use the static version of this function. + */ + public final void disableSystemWide() { + disableSystemWide(mPropertyName); + } + + /** + * Disable all caches system-wide that are keyed on {@var name}. This + * function is synchronous: caches are invalidated and disabled upon return. + * + * @param name Name of the cache-key property to invalidate + */ + public static void disableSystemWide(@NonNull String name) { + SystemProperties.set(name, Long.toString(NONCE_DISABLED)); + } + + /** + * Non-static convenience version of invalidateCache() for situations in which only a single + * PropertyInvalidatedCache is keyed on a particular property value. + */ + public final void invalidateCache() { + invalidateCache(mPropertyName); + } + + /** + * Invalidate PropertyInvalidatedCache caches in all processes that are keyed on + * {@var name}. This function is synchronous: caches are invalidated upon return. + * + * @param name Name of the cache-key property to invalidate + */ + public static void invalidateCache(@NonNull String name) { + // There's no race here: we don't require that values strictly increase, but instead + // only that each is unique in a single runtime-restart session. + final long nonce = SystemProperties.getLong(name, NONCE_UNSET); + if (nonce == NONCE_DISABLED) { + if (DEBUG) { + Log.d(TAG, "refusing to invalidate disabled cache: " + name); + } + return; + } + long newValue; + do { + newValue = NoPreloadHolder.next(); + } while (newValue == NONCE_UNSET || newValue == NONCE_DISABLED); + final String newValueString = Long.toString(newValue); + if (DEBUG) { + Log.d(TAG, + String.format("invalidating cache [%s]: [%s] -> [%s]", + name, + nonce, + newValueString)); + } + SystemProperties.set(name, newValueString); + } +} diff --git a/core/java/android/app/compat/ChangeIdStateCache.java b/core/java/android/app/compat/ChangeIdStateCache.java new file mode 100644 index 000000000000..9ef63f6587f0 --- /dev/null +++ b/core/java/android/app/compat/ChangeIdStateCache.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 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 android.app.compat; + +import android.app.PropertyInvalidatedCache; +import android.content.Context; +import android.os.Binder; +import android.os.RemoteException; +import android.os.ServiceManager; + +import com.android.internal.compat.IPlatformCompat; + +/** + * Handles caching of calls to {@link com.android.internal.compat.IPlatformCompat} + * @hide + */ +public final class ChangeIdStateCache + extends PropertyInvalidatedCache<ChangeIdStateQuery, Boolean> { + private static final String CACHE_KEY = "cache_key.is_compat_change_enabled"; + private static final int MAX_ENTRIES = 20; + private static boolean sDisabled = false; + + /** @hide */ + public ChangeIdStateCache() { + super(MAX_ENTRIES, CACHE_KEY); + } + + /** + * Disable cache. + * + * <p>Should only be used in unit tests. + * @hide + */ + public static void disable() { + sDisabled = true; + } + + /** + * Invalidate the cache. + * + * <p>Can only be called by the system server process. + * @hide + */ + public static void invalidate() { + if (!sDisabled) { + PropertyInvalidatedCache.invalidateCache(CACHE_KEY); + } + } + + @Override + protected Boolean recompute(ChangeIdStateQuery query) { + IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface( + ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + final long token = Binder.clearCallingIdentity(); + try { + if (query.type == ChangeIdStateQuery.QUERY_BY_PACKAGE_NAME) { + return platformCompat.isChangeEnabledByPackageName(query.changeId, + query.packageName, + query.userId); + } else if (query.type == ChangeIdStateQuery.QUERY_BY_UID) { + return platformCompat.isChangeEnabledByUid(query.changeId, query.uid); + } else { + throw new IllegalArgumentException("Invalid query type: " + query.type); + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } finally { + Binder.restoreCallingIdentity(token); + } + throw new IllegalStateException("Could not recompute value!"); + } +} diff --git a/core/java/android/app/compat/ChangeIdStateQuery.java b/core/java/android/app/compat/ChangeIdStateQuery.java new file mode 100644 index 000000000000..2c4c120672ab --- /dev/null +++ b/core/java/android/app/compat/ChangeIdStateQuery.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 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 android.app.compat; + +import android.annotation.IntDef; +import android.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + + +/** + * A key type for caching calls to {@link com.android.internal.compat.IPlatformCompat} + * + * <p>For {@link com.android.internal.compat.IPlatformCompat#isChangeEnabledByPackageName} + * and {@link com.android.internal.compat.IPlatformCompat#isChangeEnabledByUid} + * + * @hide + */ +final class ChangeIdStateQuery { + + static final int QUERY_BY_PACKAGE_NAME = 0; + static final int QUERY_BY_UID = 1; + @IntDef({QUERY_BY_PACKAGE_NAME, QUERY_BY_UID}) + @Retention(RetentionPolicy.SOURCE) + @interface QueryType {} + + public @QueryType int type; + public long changeId; + public String packageName; + public int uid; + public int userId; + + private ChangeIdStateQuery(@QueryType int type, long changeId, String packageName, + int uid, int userId) { + this.type = type; + this.changeId = changeId; + this.packageName = packageName; + this.uid = uid; + this.userId = userId; + } + + static ChangeIdStateQuery byPackageName(long changeId, @NonNull String packageName, + int userId) { + return new ChangeIdStateQuery(QUERY_BY_PACKAGE_NAME, changeId, packageName, 0, userId); + } + + static ChangeIdStateQuery byUid(long changeId, int uid) { + return new ChangeIdStateQuery(QUERY_BY_UID, changeId, null, uid, 0); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if ((other == null) || !(other instanceof ChangeIdStateQuery)) { + return false; + } + final ChangeIdStateQuery that = (ChangeIdStateQuery) other; + return this.type == that.type + && this.changeId == that.changeId + && Objects.equals(this.packageName, that.packageName) + && this.uid == that.uid + && this.userId == that.userId; + } + + @Override + public int hashCode() { + return Objects.hash(type, changeId, packageName, uid, userId); + } +} diff --git a/core/java/android/app/compat/CompatChanges.java b/core/java/android/app/compat/CompatChanges.java index e289a2775b79..0d5e45f9e5d4 100644 --- a/core/java/android/app/compat/CompatChanges.java +++ b/core/java/android/app/compat/CompatChanges.java @@ -19,14 +19,8 @@ package android.app.compat; import android.annotation.NonNull; import android.annotation.SystemApi; import android.compat.Compatibility; -import android.content.Context; -import android.os.Binder; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.UserHandle; -import com.android.internal.compat.IPlatformCompat; - /** * CompatChanges APIs - to be used by platform code only (including mainline * modules). @@ -35,6 +29,7 @@ import com.android.internal.compat.IPlatformCompat; */ @SystemApi public final class CompatChanges { + private static final ChangeIdStateCache QUERY_CACHE = new ChangeIdStateCache(); private CompatChanges() {} /** @@ -69,17 +64,8 @@ public final class CompatChanges { */ public static boolean isChangeEnabled(long changeId, @NonNull String packageName, @NonNull UserHandle user) { - IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface( - ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); - final long token = Binder.clearCallingIdentity(); - try { - return platformCompat.isChangeEnabledByPackageName(changeId, packageName, - user.getIdentifier()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } finally { - Binder.restoreCallingIdentity(token); - } + return QUERY_CACHE.query(ChangeIdStateQuery.byPackageName(changeId, packageName, + user.getIdentifier())); } /** @@ -101,15 +87,7 @@ public final class CompatChanges { * @return {@code true} if the change is enabled for the current app. */ public static boolean isChangeEnabled(long changeId, int uid) { - IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface( - ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); - final long token = Binder.clearCallingIdentity(); - try { - return platformCompat.isChangeEnabledByUid(changeId, uid); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } finally { - Binder.restoreCallingIdentity(token); - } + return QUERY_CACHE.query(ChangeIdStateQuery.byUid(changeId, uid)); } + } diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index cb3140487f35..5405ad37ffda 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -4716,19 +4716,19 @@ public class ConnectivityManager { /** * Returns the {@code uid} of the owner of a network connection. * - * @param protocol The protocol of the connection. Only {@code IPPROTO_TCP} and - * {@code IPPROTO_UDP} currently supported. + * @param protocol The protocol of the connection. Only {@code IPPROTO_TCP} and {@code + * IPPROTO_UDP} currently supported. * @param local The local {@link InetSocketAddress} of a connection. * @param remote The remote {@link InetSocketAddress} of a connection. - * * @return {@code uid} if the connection is found and the app has permission to observe it - * (e.g., if it is associated with the calling VPN app's tunnel) or - * {@link android.os.Process#INVALID_UID} if the connection is not found. - * Throws {@link SecurityException} if the caller is not the active VPN for the current user. - * Throws {@link IllegalArgumentException} if an unsupported protocol is requested. - */ - public int getConnectionOwnerUid(int protocol, @NonNull InetSocketAddress local, - @NonNull InetSocketAddress remote) { + * (e.g., if it is associated with the calling VPN app's VpnService tunnel) or {@link + * android.os.Process#INVALID_UID} if the connection is not found. + * @throws {@link SecurityException} if the caller is not the active VpnService for the current + * user. + * @throws {@link IllegalArgumentException} if an unsupported protocol is requested. + */ + public int getConnectionOwnerUid( + int protocol, @NonNull InetSocketAddress local, @NonNull InetSocketAddress remote) { ConnectionInfo connectionInfo = new ConnectionInfo(protocol, local, remote); try { return mService.getConnectionOwnerUid(connectionInfo); diff --git a/core/java/android/os/image/DynamicSystemManager.java b/core/java/android/os/image/DynamicSystemManager.java index cbf531c5730a..17851adac51b 100644 --- a/core/java/android/os/image/DynamicSystemManager.java +++ b/core/java/android/os/image/DynamicSystemManager.java @@ -19,6 +19,7 @@ package android.os.image; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.content.Context; +import android.gsi.AvbPublicKey; import android.gsi.GsiProgress; import android.os.ParcelFileDescriptor; import android.os.RemoteException; @@ -85,6 +86,23 @@ public class DynamicSystemManager { throw new RuntimeException(e.toString()); } } + + /** + * Retrieve AVB public key from installing partition. + * + * @param dst Output the AVB public key. + * @return true on success, false if partition doesn't have a + * valid VBMeta block to retrieve the AVB key from. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM) + public boolean getAvbPublicKey(AvbPublicKey dst) { + try { + return mService.getAvbPublicKey(dst); + } catch (RemoteException e) { + throw new RuntimeException(e.toString()); + } + } + /** * Finish write and make device to boot into the it after reboot. * diff --git a/core/java/android/os/image/IDynamicSystemService.aidl b/core/java/android/os/image/IDynamicSystemService.aidl index cc32f998d0c2..a1f927266de3 100644 --- a/core/java/android/os/image/IDynamicSystemService.aidl +++ b/core/java/android/os/image/IDynamicSystemService.aidl @@ -15,6 +15,7 @@ */ package android.os.image; +import android.gsi.AvbPublicKey; import android.gsi.GsiProgress; /** {@hide} */ @@ -108,4 +109,13 @@ interface IDynamicSystemService * @return true on success, false otherwise. */ boolean submitFromAshmem(long bytes); + + /** + * Retrieve AVB public key from installing partition. + * + * @param dst Output the AVB public key. + * @return true on success, false if partition doesn't have a + * valid VBMeta block to retrieve the AVB key from. + */ + boolean getAvbPublicKey(out AvbPublicKey dst); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 64dcfa7d1266..0a2c59f765f3 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1640,6 +1640,7 @@ <!-- Allows Settings and SystemUI to call methods in Networking services <p>Not for use by third-party or privileged applications. + @SystemApi @TestApi @hide This should only be used by Settings and SystemUI. --> <permission android:name="android.permission.NETWORK_SETTINGS" diff --git a/core/tests/systemproperties/src/android/os/PropertyInvalidatedCacheTest.java b/core/tests/systemproperties/src/android/os/PropertyInvalidatedCacheTest.java new file mode 100644 index 000000000000..c4080e822a3f --- /dev/null +++ b/core/tests/systemproperties/src/android/os/PropertyInvalidatedCacheTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2019 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 android.os; + +import android.app.PropertyInvalidatedCache; +import android.test.suitebuilder.annotation.SmallTest; + +import junit.framework.TestCase; + +public class PropertyInvalidatedCacheTest extends TestCase { + private static final String KEY = "sys.testkey"; + private static final String UNSET_KEY = "Aiw7woh6ie4toh7W"; + + private static class TestCache extends PropertyInvalidatedCache<Integer, String> { + TestCache() { + this(KEY); + } + + TestCache(String key) { + super(4, key); + } + + @Override + protected String recompute(Integer qv) { + mRecomputeCount += 1; + return "foo" + qv.toString(); + } + + int getRecomputeCount() { + return mRecomputeCount; + } + + private int mRecomputeCount = 0; + } + + @Override + protected void setUp() { + SystemProperties.set(KEY, ""); + } + + @SmallTest + public void testCacheRecompute() throws Exception { + TestCache cache = new TestCache(); + cache.invalidateCache(); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo6", cache.query(6)); + assertEquals(2, cache.getRecomputeCount()); + cache.invalidateCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + } + + @SmallTest + public void testCacheInitialState() throws Exception { + TestCache cache = new TestCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(2, cache.getRecomputeCount()); + cache.invalidateCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + } + + @SmallTest + public void testCachePropertyUnset() throws Exception { + TestCache cache = new TestCache(UNSET_KEY); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(2, cache.getRecomputeCount()); + } + + @SmallTest + public void testCacheDisableState() throws Exception { + TestCache cache = new TestCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(2, cache.getRecomputeCount()); + cache.invalidateCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + cache.disableSystemWide(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(5, cache.getRecomputeCount()); + cache.invalidateCache(); // Should not reenable + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(7, cache.getRecomputeCount()); + } + + @SmallTest + public void testRefreshSameObject() throws Exception { + int[] refreshCount = new int[1]; + TestCache cache = new TestCache() { + @Override + protected String refresh(String oldResult, Integer query) { + refreshCount[0] += 1; + return oldResult; + } + }; + cache.invalidateCache(); + String result1 = cache.query(5); + assertEquals("foo5", result1); + String result2 = cache.query(5); + assertSame(result1, result2); + assertEquals(1, cache.getRecomputeCount()); + assertEquals(1, refreshCount[0]); + assertEquals("foo5", cache.query(5)); + assertEquals(2, refreshCount[0]); + } + + @SmallTest + public void testRefreshInvalidateRace() throws Exception { + int[] refreshCount = new int[1]; + TestCache cache = new TestCache() { + @Override + protected String refresh(String oldResult, Integer query) { + refreshCount[0] += 1; + invalidateCache(); + return new String(oldResult); + } + }; + cache.invalidateCache(); + String result1 = cache.query(5); + assertEquals("foo5", result1); + String result2 = cache.query(5); + assertEquals(result1, result2); + assertNotSame(result1, result2); + assertEquals(2, cache.getRecomputeCount()); + } + + @SmallTest + public void testLocalProcessDisable() throws Exception { + TestCache cache = new TestCache(); + cache.invalidateCache(); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals(cache.isDisabledLocal(), false); + cache.disableLocal(); + assertEquals(cache.isDisabledLocal(), true); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + } + +} diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 1ce9d9157a54..5f032fc90dfe 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -7528,6 +7528,13 @@ public class ConnectivityService extends IConnectivityManager.Stub */ public int getConnectionOwnerUid(ConnectionInfo connectionInfo) { final Vpn vpn = enforceActiveVpnOrNetworkStackPermission(); + + // Only VpnService based VPNs should be able to get this information. + if (vpn != null && vpn.getActiveAppVpnType() != VpnManager.TYPE_VPN_SERVICE) { + throw new SecurityException( + "getConnectionOwnerUid() not allowed for non-VpnService VPNs"); + } + if (connectionInfo.protocol != IPPROTO_TCP && connectionInfo.protocol != IPPROTO_UDP) { throw new IllegalArgumentException("Unsupported protocol " + connectionInfo.protocol); } diff --git a/services/core/java/com/android/server/DynamicSystemService.java b/services/core/java/com/android/server/DynamicSystemService.java index c60460fccb76..41207c97492a 100644 --- a/services/core/java/com/android/server/DynamicSystemService.java +++ b/services/core/java/com/android/server/DynamicSystemService.java @@ -18,6 +18,7 @@ package com.android.server; import android.content.Context; import android.content.pm.PackageManager; +import android.gsi.AvbPublicKey; import android.gsi.GsiProgress; import android.gsi.IGsiService; import android.gsi.IGsid; @@ -227,4 +228,13 @@ public class DynamicSystemService extends IDynamicSystemService.Stub implements throw new RuntimeException(e.toString()); } } + + @Override + public boolean getAvbPublicKey(AvbPublicKey dst) { + try { + return getGsiService().getAvbPublicKey(dst) == 0; + } catch (RemoteException e) { + throw new RuntimeException(e.toString()); + } + } } diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java index f15d999e1006..cc5b7668e028 100644 --- a/services/core/java/com/android/server/compat/CompatConfig.java +++ b/services/core/java/com/android/server/compat/CompatConfig.java @@ -16,6 +16,7 @@ package com.android.server.compat; +import android.app.compat.ChangeIdStateCache; import android.compat.Compatibility.ChangeConfig; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -78,6 +79,7 @@ final class CompatConfig { void addChange(CompatChange change) { synchronized (mChanges) { mChanges.put(change.getId(), change); + invalidateCache(); } } @@ -170,6 +172,7 @@ final class CompatConfig { addChange(c); } c.addPackageOverride(packageName, enabled); + invalidateCache(); } return alreadyKnown; } @@ -226,6 +229,7 @@ final class CompatConfig { // Should never occur, since validator is in the same process. throw new RuntimeException("Unable to call override validator!", e); } + invalidateCache(); } return overrideExists; } @@ -248,6 +252,7 @@ final class CompatConfig { addOverride(changeId, packageName, false); } + invalidateCache(); } } @@ -277,6 +282,7 @@ final class CompatConfig { throw new RuntimeException("Unable to call override validator!", e); } } + invalidateCache(); } } @@ -396,4 +402,8 @@ final class CompatConfig { IOverrideValidator getOverrideValidator() { return mOverrideValidator; } + + private void invalidateCache() { + ChangeIdStateCache.invalidate(); + } } diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java index a5d9aa2c9a46..85b8ec699f02 100644 --- a/services/core/java/com/android/server/compat/PlatformCompat.java +++ b/services/core/java/com/android/server/compat/PlatformCompat.java @@ -234,8 +234,8 @@ public class PlatformCompat extends IPlatformCompat.Stub { @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - checkCompatChangeReadAndLogPermission(); if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, "platform_compat", pw)) return; + checkCompatChangeReadAndLogPermission(); mCompatConfig.dumpConfig(pw); } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 7f6dc55a369d..8692436f92e4 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -794,10 +794,10 @@ public class Vpn { // ignore } mContext.unbindService(mConnection); - mConnection = null; + cleanupVpnStateLocked(); } else if (mVpnRunner != null) { + // cleanupVpnStateLocked() is called from mVpnRunner.exit() mVpnRunner.exit(); - mVpnRunner = null; } try { @@ -1104,7 +1104,6 @@ public class Vpn { */ public synchronized ParcelFileDescriptor establish(VpnConfig config) { // Check if the caller is already prepared. - UserManager mgr = UserManager.get(mContext); if (Binder.getCallingUid() != mOwnerUID) { return null; } @@ -1118,10 +1117,7 @@ public class Vpn { long token = Binder.clearCallingIdentity(); try { // Restricted users are not allowed to create VPNs, they are tied to Owner - UserInfo user = mgr.getUserInfo(mUserHandle); - if (user.isRestricted()) { - throw new SecurityException("Restricted users cannot establish VPNs"); - } + enforceNotRestrictedUser(); ResolveInfo info = AppGlobals.getPackageManager().resolveService(intent, null, 0, mUserHandle); @@ -1543,24 +1539,30 @@ public class Vpn { public void interfaceRemoved(String interfaze) { synchronized (Vpn.this) { if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) { - mStatusIntent = null; - mNetworkCapabilities.setUids(null); - mConfig = null; - mInterface = null; if (mConnection != null) { mContext.unbindService(mConnection); - mConnection = null; - agentDisconnect(); + cleanupVpnStateLocked(); } else if (mVpnRunner != null) { - // agentDisconnect must be called from mVpnRunner.exit() + // cleanupVpnStateLocked() is called from mVpnRunner.exit() mVpnRunner.exit(); - mVpnRunner = null; } } } } }; + private void cleanupVpnStateLocked() { + mStatusIntent = null; + mNetworkCapabilities.setUids(null); + mConfig = null; + mInterface = null; + + // Unconditionally clear both VpnService and VpnRunner fields. + mVpnRunner = null; + mConnection = null; + agentDisconnect(); + } + private void enforceControlPermission() { mContext.enforceCallingPermission(Manifest.permission.CONTROL_VPN, "Unauthorized Caller"); } @@ -1673,6 +1675,25 @@ public class Vpn { } /** + * Gets the currently running App-based VPN type + * + * @return the {@link VpnManager.VpnType}. {@link VpnManager.TYPE_VPN_NONE} if not running an + * app-based VPN. While VpnService-based VPNs are always app VPNs and LegacyVpn is always + * Settings-based, the Platform VPNs can be initiated by both apps and Settings. + */ + public synchronized int getActiveAppVpnType() { + if (VpnConfig.LEGACY_VPN.equals(mPackage)) { + return VpnManager.TYPE_VPN_NONE; + } + + if (mVpnRunner != null && mVpnRunner instanceof IkeV2VpnRunner) { + return VpnManager.TYPE_VPN_PLATFORM; + } else { + return VpnManager.TYPE_VPN_SERVICE; + } + } + + /** * @param uid The target uid. * * @return {@code true} if {@code uid} is included in one of the mBlockedUidsAsToldToNetd @@ -1800,6 +1821,17 @@ public class Vpn { throw new IllegalStateException("Unable to find IPv4 default gateway"); } + private void enforceNotRestrictedUser() { + Binder.withCleanCallingIdentity(() -> { + final UserManager mgr = UserManager.get(mContext); + final UserInfo user = mgr.getUserInfo(mUserHandle); + + if (user.isRestricted()) { + throw new SecurityException("Restricted users cannot configure VPNs"); + } + }); + } + /** * Start legacy VPN, controlling native daemons as needed. Creates a * secondary thread to perform connection work, returning quickly. @@ -2020,7 +2052,25 @@ public class Vpn { public abstract void run(); - protected abstract void exit(); + /** + * Disconnects the NetworkAgent and cleans up all state related to the VpnRunner. + * + * <p>All outer Vpn instance state is cleaned up in cleanupVpnStateLocked() + */ + protected abstract void exitVpnRunner(); + + /** + * Triggers the cleanup of the VpnRunner, and additionally cleans up Vpn instance-wide state + * + * <p>This method ensures that simple calls to exit() will always clean up global state + * properly. + */ + protected final void exit() { + synchronized (Vpn.this) { + exitVpnRunner(); + cleanupVpnStateLocked(); + } + } } interface IkeV2VpnRunnerCallback { @@ -2350,17 +2400,6 @@ public class Vpn { } /** - * Triggers cleanup of outer class' state - * - * <p>Can be called from any thread, as it does not mutate state in the Ikev2VpnRunner. - */ - private void cleanupVpnState() { - synchronized (Vpn.this) { - agentDisconnect(); - } - } - - /** * Cleans up all Ikev2VpnRunner internal state * * <p>This method MUST always be called on the mExecutor thread in order to ensure @@ -2379,10 +2418,7 @@ public class Vpn { } @Override - public void exit() { - // Cleanup outer class' state immediately, otherwise race conditions may ensue. - cleanupVpnState(); - + public void exitVpnRunner() { mExecutor.execute(() -> { shutdownVpnRunner(); }); @@ -2481,10 +2517,9 @@ public class Vpn { /** Tears down this LegacyVpn connection */ @Override - public void exit() { + public void exitVpnRunner() { // We assume that everything is reset after stopping the daemons. interrupt(); - agentDisconnect(); try { mContext.unregisterReceiver(mBroadcastReceiver); } catch (IllegalArgumentException e) {} @@ -2757,6 +2792,7 @@ public class Vpn { checkNotNull(keyStore, "KeyStore missing"); verifyCallingUidAndPackage(packageName); + enforceNotRestrictedUser(); final byte[] encodedProfile = profile.encode(); if (encodedProfile.length > MAX_VPN_PROFILE_SIZE_BYTES) { @@ -2792,6 +2828,7 @@ public class Vpn { checkNotNull(keyStore, "KeyStore missing"); verifyCallingUidAndPackage(packageName); + enforceNotRestrictedUser(); Binder.withCleanCallingIdentity( () -> { @@ -2834,6 +2871,8 @@ public class Vpn { checkNotNull(packageName, "No package name provided"); checkNotNull(keyStore, "KeyStore missing"); + enforceNotRestrictedUser(); + // Prepare VPN for startup if (!prepare(packageName, null /* newPackage */, VpnManager.TYPE_VPN_PLATFORM)) { throw new SecurityException("User consent not granted for package " + packageName); @@ -2899,6 +2938,8 @@ public class Vpn { public synchronized void stopVpnProfile(@NonNull String packageName) { checkNotNull(packageName, "No package name provided"); + enforceNotRestrictedUser(); + // To stop the VPN profile, the caller must be the current prepared package and must be // running an Ikev2VpnProfile. if (!isCurrentPreparedPackage(packageName) && mVpnRunner instanceof IkeV2VpnRunner) { diff --git a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java index 407f67e2fd8e..44f4ccf69cdd 100644 --- a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; +import android.app.compat.ChangeIdStateCache; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -74,6 +75,7 @@ public class CompatConfigTest { // Assume userdebug/eng non-final build when(mBuildClassifier.isDebuggableBuild()).thenReturn(true); when(mBuildClassifier.isFinalBuild()).thenReturn(false); + ChangeIdStateCache.disable(); } @Test diff --git a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java index ce5d6d9be770..717a78de7497 100644 --- a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java +++ b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java @@ -57,6 +57,7 @@ public class PlatformCompatTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + android.app.compat.ChangeIdStateCache.disable(); when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mPackageManager.getPackageUid(eq(PACKAGE_NAME), eq(0))).thenThrow( new PackageManager.NameNotFoundException()); diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java index 6fb4612245f1..8ed497b78b3c 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -78,6 +78,7 @@ import static android.net.NetworkPolicyManager.RULE_NONE; import static android.net.NetworkPolicyManager.RULE_REJECT_ALL; import static android.net.NetworkPolicyManager.RULE_REJECT_METERED; import static android.net.RouteInfo.RTN_UNREACHABLE; +import static android.system.OsConstants.IPPROTO_TCP; import static com.android.server.ConnectivityServiceTestUtilsKt.transportToLegacyType; import static com.android.testutils.ConcurrentUtilsKt.await; @@ -138,6 +139,7 @@ import android.content.pm.UserInfo; import android.content.res.Resources; import android.location.LocationManager; import android.net.CaptivePortalData; +import android.net.ConnectionInfo; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.ConnectivityManager.PacketKeepalive; @@ -153,6 +155,7 @@ import android.net.INetworkMonitorCallbacks; import android.net.INetworkPolicyListener; import android.net.INetworkPolicyManager; import android.net.INetworkStatsService; +import android.net.InetAddresses; import android.net.InterfaceConfiguration; import android.net.IpPrefix; import android.net.IpSecManager; @@ -176,6 +179,7 @@ import android.net.RouteInfo; import android.net.SocketKeepalive; import android.net.UidRange; import android.net.Uri; +import android.net.VpnManager; import android.net.metrics.IpConnectivityLog; import android.net.shared.NetworkMonitorUtils; import android.net.shared.PrivateDnsConfig; @@ -272,6 +276,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; +import java.util.function.Supplier; import kotlin.reflect.KClass; @@ -445,15 +450,21 @@ public class ConnectivityServiceTest { return mPackageManager; } + private int checkMockedPermission(String permission, Supplier<Integer> ifAbsent) { + final Integer granted = mMockedPermissions.get(permission); + return granted != null ? granted : ifAbsent.get(); + } + @Override public int checkPermission(String permission, int pid, int uid) { - final Integer granted = mMockedPermissions.get(permission); - if (granted == null) { - // All non-mocked permissions should be held by the test or unnecessary: check as - // normal to make sure the code does not rely on unexpected permissions. - return super.checkPermission(permission, pid, uid); - } - return granted; + return checkMockedPermission( + permission, () -> super.checkPermission(permission, pid, uid)); + } + + @Override + public int checkCallingOrSelfPermission(String permission) { + return checkMockedPermission( + permission, () -> super.checkCallingOrSelfPermission(permission)); } @Override @@ -1002,6 +1013,7 @@ public class ConnectivityServiceTest { // Careful ! This is different from mNetworkAgent, because MockNetworkAgent does // not inherit from NetworkAgent. private TestNetworkAgentWrapper mMockNetworkAgent; + private int mVpnType = VpnManager.TYPE_VPN_SERVICE; private VpnInfo mVpnInfo; @@ -1022,6 +1034,10 @@ public class ConnectivityServiceTest { updateCapabilities(null /* defaultNetwork */); } + public void setVpnType(int vpnType) { + mVpnType = vpnType; + } + @Override public int getNetId() { if (mMockNetworkAgent == null) { @@ -1040,6 +1056,11 @@ public class ConnectivityServiceTest { return mConnected; // Similar trickery } + @Override + public int getActiveAppVpnType() { + return mVpnType; + } + private void connect(boolean isAlwaysMetered) { mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities()); mConnected = true; @@ -6429,6 +6450,90 @@ public class ConnectivityServiceTest { assertEquals(Process.INVALID_UID, newNc.getOwnerUid()); } + private void setupConnectionOwnerUid(int vpnOwnerUid, @VpnManager.VpnType int vpnType) + throws Exception { + final Set<UidRange> vpnRange = Collections.singleton(UidRange.createForUser(VPN_USER)); + establishVpn(new LinkProperties(), vpnOwnerUid, vpnRange); + mMockVpn.setVpnType(vpnType); + + final VpnInfo vpnInfo = new VpnInfo(); + vpnInfo.ownerUid = vpnOwnerUid; + mMockVpn.setVpnInfo(vpnInfo); + } + + private void setupConnectionOwnerUidAsVpnApp(int vpnOwnerUid, @VpnManager.VpnType int vpnType) + throws Exception { + setupConnectionOwnerUid(vpnOwnerUid, vpnType); + + // Test as VPN app + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + mServiceContext.setPermission( + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_DENIED); + } + + private ConnectionInfo getTestConnectionInfo() throws Exception { + return new ConnectionInfo( + IPPROTO_TCP, + new InetSocketAddress(InetAddresses.parseNumericAddress("1.2.3.4"), 1234), + new InetSocketAddress(InetAddresses.parseNumericAddress("2.3.4.5"), 2345)); + } + + @Test + public void testGetConnectionOwnerUidPlatformVpn() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_PLATFORM); + + try { + mService.getConnectionOwnerUid(getTestConnectionInfo()); + fail("Expected SecurityException for non-VpnService app"); + } catch (SecurityException expected) { + } + } + + @Test + public void testGetConnectionOwnerUidVpnServiceWrongUser() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUidAsVpnApp(myUid + 1, VpnManager.TYPE_VPN_SERVICE); + + try { + mService.getConnectionOwnerUid(getTestConnectionInfo()); + fail("Expected SecurityException for non-VpnService app"); + } catch (SecurityException expected) { + } + } + + @Test + public void testGetConnectionOwnerUidVpnServiceDoesNotThrow() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_SERVICE); + + // TODO: Test the returned UID + mService.getConnectionOwnerUid(getTestConnectionInfo()); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceNetworkStackDoesNotThrow() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE); + mServiceContext.setPermission( + android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED); + + // TODO: Test the returned UID + mService.getConnectionOwnerUid(getTestConnectionInfo()); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceMainlineNetworkStackDoesNotThrow() + throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE); + mServiceContext.setPermission( + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED); + + // TODO: Test the returned UID + mService.getConnectionOwnerUid(getTestConnectionInfo()); + } + private TestNetworkAgentWrapper establishVpn( LinkProperties lp, int ownerUid, Set<UidRange> vpnRange) throws Exception { final TestNetworkAgentWrapper diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index eb78529e8715..ac1c51837e93 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -656,8 +656,12 @@ public class VpnTest { } private Vpn createVpnAndSetupUidChecks(int... grantedOps) throws Exception { - final Vpn vpn = createVpn(primaryUser.id); - setMockedUsers(primaryUser); + return createVpnAndSetupUidChecks(primaryUser, grantedOps); + } + + private Vpn createVpnAndSetupUidChecks(UserInfo user, int... grantedOps) throws Exception { + final Vpn vpn = createVpn(user.id); + setMockedUsers(user); when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt())) .thenReturn(Process.myUid()); @@ -726,6 +730,19 @@ public class VpnTest { } @Test + public void testProvisionVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN); + + try { + vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile, mKeyStore); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test public void testDeleteVpnProfile() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(); @@ -736,6 +753,19 @@ public class VpnTest { } @Test + public void testDeleteVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN); + + try { + vpn.deleteVpnProfile(TEST_VPN_PKG, mKeyStore); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test public void testGetVpnProfilePrivileged() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(); @@ -820,6 +850,32 @@ public class VpnTest { } @Test + public void testStartVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN); + + try { + vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testStopVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN); + + try { + vpn.stopVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test public void testSetPackageAuthorizationVpnService() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(); |