diff options
| author | 2024-10-25 15:09:41 +0000 | |
|---|---|---|
| committer | 2024-10-25 15:09:41 +0000 | |
| commit | d64d10c280fdf14b5991e5275959b9f6e3dad96b (patch) | |
| tree | 15e93b39b178fa546a4b2851895825eb896e8335 | |
| parent | 826bd88898aab75d32c175560f3238665a4817ad (diff) | |
| parent | 7351dd7183db247f5fd8ab14bc798f3107d4bdd2 (diff) | |
Merge "Put PIC nonces in shared memory" into main
| -rw-r--r-- | core/java/android/app/PropertyInvalidatedCache.java | 484 | ||||
| -rw-r--r-- | core/java/android/app/performance.aconfig | 11 | ||||
| -rw-r--r-- | core/java/com/android/internal/os/ApplicationSharedMemory.java | 31 | ||||
| -rw-r--r-- | core/jni/Android.bp | 1 | ||||
| -rw-r--r-- | core/jni/AndroidRuntime.cpp | 2 | ||||
| -rw-r--r-- | core/jni/android_app_PropertyInvalidatedCache.cpp | 119 | ||||
| -rw-r--r-- | core/jni/android_app_PropertyInvalidatedCache.h | 146 | ||||
| -rw-r--r-- | core/jni/com_android_internal_os_ApplicationSharedMemory.cpp | 24 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java | 63 |
9 files changed, 862 insertions, 19 deletions
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index c93a6dd8969e..bc9e709420f1 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -30,16 +30,23 @@ import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemClock; import android.os.SystemProperties; +import android.util.ArrayMap; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.ApplicationSharedMemory; import com.android.internal.os.BackgroundThread; +import dalvik.annotation.optimization.CriticalNative; +import dalvik.annotation.optimization.FastNative; + import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -203,19 +210,14 @@ public class PropertyInvalidatedCache<Query, Result> { }; /** - * Verify that the property name conforms to the standard. Log a warning if this is not true. - * Note that this is done once in the cache constructor; it does not have to be very fast. + * Verify that the property name conforms to the standard and throw if this is not true. Note + * that this is done only once for a given property name; it does not have to be very fast. */ - private void validateCacheKey(String name) { - if (Build.IS_USER) { - // Do not bother checking keys in user builds. The keys will have been tested in - // eng/userdebug builds already. - return; - } + private static void throwIfInvalidCacheKey(String name) { for (int i = 0; i < sValidKeyPrefix.length; i++) { if (name.startsWith(sValidKeyPrefix[i])) return; } - Log.w(TAG, "invalid cache name: " + name); + throw new IllegalArgumentException("invalid cache name: " + name); } /** @@ -234,7 +236,8 @@ public class PropertyInvalidatedCache<Query, Result> { * reserved values cause the cache to be skipped. */ // This is the initial value of all cache keys. It is changed when a cache is invalidated. - private static final int NONCE_UNSET = 0; + @VisibleForTesting + static final int NONCE_UNSET = 0; // This value is used in two ways. First, it is used internally to indicate that the cache is // disabled for the current query. Secondly, it is used to globally disable the cache across // the entire system. Once a cache is disabled, there is no way to enable it again. The @@ -685,6 +688,77 @@ public class PropertyInvalidatedCache<Query, Result> { } /** + * Manage nonces that are stored in shared memory. + */ + private static final class NonceSharedMem extends NonceHandler { + // The shared memory. + private volatile NonceStore mStore; + + // The index of the nonce in shared memory. + private volatile int mHandle = NonceStore.INVALID_NONCE_INDEX; + + // True if the string has been stored, ever. + private volatile boolean mRecorded = false; + + // A short name that is saved in shared memory. This is the portion of the property name + // that follows the prefix. + private final String mShortName; + + NonceSharedMem(@NonNull String name, @Nullable String prefix) { + super(name); + if ((prefix != null) && name.startsWith(prefix)) { + mShortName = name.substring(prefix.length()); + } else { + mShortName = name; + } + } + + // Fetch the nonce from shared memory. If the shared memory is not available, return + // UNSET. If the shared memory is available but the nonce name is not known (it may not + // have been invalidated by the server yet), return UNSET. + @Override + long getNonceInternal() { + if (mHandle == NonceStore.INVALID_NONCE_INDEX) { + if (mStore == null) { + mStore = NonceStore.getInstance(); + if (mStore == null) { + return NONCE_UNSET; + } + } + mHandle = mStore.getHandleForName(mShortName); + if (mHandle == NonceStore.INVALID_NONCE_INDEX) { + return NONCE_UNSET; + } + } + return mStore.getNonce(mHandle); + } + + // Set the nonce in shared mmory. If the shared memory is not available, throw an + // exception. Otherwise, if the nonce name has never been recorded, record it now and + // fetch the handle for the name. If the handle cannot be created, throw an exception. + @Override + void setNonceInternal(long value) { + if (mHandle == NonceStore.INVALID_NONCE_INDEX || !mRecorded) { + if (mStore == null) { + mStore = NonceStore.getInstance(); + if (mStore == null) { + throw new IllegalStateException("setNonce: shared memory not ready"); + } + } + // Always store the name before fetching the handle. storeName() is idempotent + // but does take a little time, so this code calls it just once. + mStore.storeName(mShortName); + mRecorded = true; + mHandle = mStore.getHandleForName(mShortName); + if (mHandle == NonceStore.INVALID_NONCE_INDEX) { + throw new IllegalStateException("setNonce: shared memory store failed"); + } + } + mStore.setNonce(mHandle, value); + } + } + + /** * SystemProperties and shared storage are protected and cannot be written by random * processes. So, for testing purposes, the NonceLocal handler stores the nonce locally. The * NonceLocal uses the mTestNonce in the superclass, regardless of test mode. @@ -712,6 +786,7 @@ public class PropertyInvalidatedCache<Query, Result> { * Complete key prefixes. */ private static final String PREFIX_TEST = CACHE_KEY_PREFIX + "." + MODULE_TEST + "."; + private static final String PREFIX_SYSTEM = CACHE_KEY_PREFIX + "." + MODULE_SYSTEM + "."; /** * A static list of nonce handlers, indexed by name. NonceHandlers can be safely shared by @@ -722,16 +797,32 @@ public class PropertyInvalidatedCache<Query, Result> { private static final ConcurrentHashMap<String, NonceHandler> sHandlers = new ConcurrentHashMap<>(); + // True if shared memory is flag-enabled, false otherwise. Read the flags exactly once. + private static final boolean sSharedMemoryAvailable = + com.android.internal.os.Flags.applicationSharedMemoryEnabled() + && android.app.Flags.picUsesSharedMemory(); + + // Return true if this cache can use shared memory for its nonce. Shared memory may be used + // if the module is the system. + private static boolean sharedMemoryOkay(@NonNull String name) { + return sSharedMemoryAvailable && name.startsWith(PREFIX_SYSTEM); + } + /** - * Return the proper nonce handler, based on the property name. + * Return the proper nonce handler, based on the property name. A handler is created if + * necessary. Before a handler is created, the name is checked, and an exception is thrown if + * the name is not valid. */ private static NonceHandler getNonceHandler(@NonNull String name) { NonceHandler h = sHandlers.get(name); if (h == null) { synchronized (sGlobalLock) { + throwIfInvalidCacheKey(name); h = sHandlers.get(name); if (h == null) { - if (name.startsWith(PREFIX_TEST)) { + if (sharedMemoryOkay(name)) { + h = new NonceSharedMem(name, PREFIX_SYSTEM); + } else if (name.startsWith(PREFIX_TEST)) { h = new NonceLocal(name); } else { h = new NonceSysprop(name); @@ -774,7 +865,6 @@ public class PropertyInvalidatedCache<Query, Result> { public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName, @NonNull String cacheName) { mPropertyName = propertyName; - validateCacheKey(mPropertyName); mCacheName = cacheName; mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; @@ -799,7 +889,6 @@ public class PropertyInvalidatedCache<Query, Result> { public PropertyInvalidatedCache(int maxEntries, @NonNull String module, @NonNull String api, @NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) { mPropertyName = createPropertyName(module, api); - validateCacheKey(mPropertyName); mCacheName = cacheName; mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; @@ -1620,6 +1709,14 @@ public class PropertyInvalidatedCache<Query, Result> { // then only that cache is reported. boolean detail = anyDetailed(args); + if (sSharedMemoryAvailable) { + pw.println(" SharedMemory: enabled"); + NonceStore.getInstance().dump(pw, " ", detail); + } else { + pw.println(" SharedMemory: disabled"); + } + pw.println(); + ArrayList<PropertyInvalidatedCache> activeCaches = getActiveCaches(); for (int i = 0; i < activeCaches.size(); i++) { PropertyInvalidatedCache currentCache = activeCaches.get(i); @@ -1654,4 +1751,363 @@ public class PropertyInvalidatedCache<Query, Result> { Log.e(TAG, "Failed to dump PropertyInvalidatedCache instances"); } } + + /** + * Nonces in shared memory are supported by a string block that acts as a table of contents + * for nonce names, and an array of nonce values. There are two key design principles with + * respect to nonce maps: + * + * 1. It is always okay if a nonce value cannot be determined. If the nonce is UNSET, the + * cache is bypassed, which is always functionally correct. Clients do not take extraordinary + * measures to be current with the nonce map. Clients must be current with the nonce itself; + * this is achieved through the shared memory. + * + * 2. Once a name is mapped to a nonce index, the mapping is fixed for the lifetime of the + * system. It is only necessary to distinguish between the unmapped and mapped states. Once + * a client has mapped a nonce, that mapping is known to be good for the lifetime of the + * system. + * @hide + */ + @VisibleForTesting + public static class NonceStore { + + // A lock for the store. + private final Object mLock = new Object(); + + // The native pointer. This is not owned by this class. It is owned by + // ApplicationSharedMemory, and it disappears when the owning instance is closed. + private final long mPtr; + + // True if the memory is immutable. + private final boolean mMutable; + + // The maximum length of a string in the string block. The maximum length must fit in a + // byte, but a smaller value has been chosen to limit memory use. Because strings are + // run-length encoded, a string consumes at most MAX_STRING_LENGTH+1 bytes in the string + // block. + private static final int MAX_STRING_LENGTH = 63; + + // The raw byte block. Strings are stored as run-length encoded byte arrays. The first + // byte is the length of the following string. It is an axiom of the system that the + // string block is initially all zeros and that it is write-once memory: new strings are + // appended to existing strings, so there is never a need to revisit strings that have + // already been pulled from the string block. + @GuardedBy("mLock") + private final byte[] mStringBlock; + + // The expected hash code of the string block. If the hash over the string block equals + // this value, then the string block is valid. Otherwise, the block is not valid and + // should be re-read. An invalid block generally means that a client has read the shared + // memory while the server was still writing it. + @GuardedBy("mLock") + private int mBlockHash = 0; + + // The number of nonces that the native layer can hold. This is maintained for debug and + // logging. + private final int mMaxNonce; + + /** @hide */ + @VisibleForTesting + public NonceStore(long ptr, boolean mutable) { + mPtr = ptr; + mMutable = mutable; + mStringBlock = new byte[nativeGetMaxByte(ptr)]; + mMaxNonce = nativeGetMaxNonce(ptr); + refreshStringBlockLocked(); + } + + // The static lock for singleton acquisition. + private static Object sLock = new Object(); + + // NonceStore is supposed to be a singleton. + private static NonceStore sInstance; + + // Return the singleton instance. + static NonceStore getInstance() { + synchronized (sLock) { + if (sInstance == null) { + try { + ApplicationSharedMemory shmem = ApplicationSharedMemory.getInstance(); + sInstance = (shmem == null) + ? null + : new NonceStore(shmem.getSystemNonceBlock(), + shmem.isMutable()); + } catch (IllegalStateException e) { + // ApplicationSharedMemory.getInstance() throws if the shared memory is + // not yet mapped. Swallow the exception and leave sInstance null. + } + } + return sInstance; + } + } + + // The index value of an unmapped name. + public static final int INVALID_NONCE_INDEX = -1; + + // The highest string index extracted from the string block. -1 means no strings have + // been seen. This is used to skip strings that have already been processed, when the + // string block is updated. + @GuardedBy("mLock") + private int mHighestIndex = -1; + + // The number bytes of the string block that has been used. This is a statistics. + @GuardedBy("mLock") + private int mStringBytes = 0; + + // The number of partial reads on the string block. This is a statistic. + @GuardedBy("mLock") + private int mPartialReads = 0; + + // The number of times the string block was updated. This is a statistic. + @GuardedBy("mLock") + private int mStringUpdated = 0; + + // Map a string to a native index. + @GuardedBy("mLock") + private final ArrayMap<String, Integer> mStringHandle = new ArrayMap<>(); + + // Update the string map from the current string block. The string block is not modified + // and the block hash is not checked. The function skips past strings that have already + // been read, and then processes any new strings. + @GuardedBy("mLock") + private void updateStringMapLocked() { + int index = 0; + int offset = 0; + while (offset < mStringBlock.length && mStringBlock[offset] != 0) { + if (index > mHighestIndex) { + // Only record the string if it has not been seen yet. + final String s = new String(mStringBlock, offset+1, mStringBlock[offset]); + mStringHandle.put(s, index); + mHighestIndex = index; + } + offset += mStringBlock[offset] + 1; + index++; + } + mStringBytes = offset; + } + + // Append a string to the string block and update the hash. This does not write the block + // to shared memory. + @GuardedBy("mLock") + private void appendStringToMapLocked(@NonNull String str) { + int offset = 0; + while (offset < mStringBlock.length && mStringBlock[offset] != 0) { + offset += mStringBlock[offset] + 1; + } + final byte[] strBytes = str.getBytes(); + + if (offset + strBytes.length >= mStringBlock.length) { + // Overflow. Do not add the string to the block; the string will remain undefined. + return; + } + + mStringBlock[offset] = (byte) strBytes.length; + offset++; + for (int i = 0; i < strBytes.length; i++, offset++) { + mStringBlock[offset] = strBytes[i]; + } + mBlockHash = Arrays.hashCode(mStringBlock); + } + + // Possibly update the string block. If the native shared memory has a new block hash, + // then read the new string block values from shared memory, as well as the new hash. + @GuardedBy("mLock") + private void refreshStringBlockLocked() { + if (mBlockHash == nativeGetByteBlockHash(mPtr)) { + // The fastest way to know that the shared memory string block has not changed. + return; + } + final int hash = nativeGetByteBlock(mPtr, mBlockHash, mStringBlock); + if (hash != Arrays.hashCode(mStringBlock)) { + // This is a partial read: ignore it. The next time someone needs this string + // the memory will be read again and should succeed. Set the local hash to + // zero to ensure that the next read attempt will actually read from shared + // memory. + mBlockHash = 0; + mPartialReads++; + return; + } + // The hash has changed. Update the strings from the byte block. + mStringUpdated++; + mBlockHash = hash; + updateStringMapLocked(); + } + + // Throw an exception if the string cannot be stored in the string block. + private static void throwIfBadString(@NonNull String s) { + if (s.length() == 0) { + throw new IllegalArgumentException("cannot store an empty string"); + } + if (s.length() > MAX_STRING_LENGTH) { + throw new IllegalArgumentException("cannot store a string longer than " + + MAX_STRING_LENGTH); + } + } + + // Throw an exception if the nonce handle is invalid. The handle is bad if it is out of + // range of allocated handles. Note that NONCE_HANDLE_INVALID will throw: this is + // important for setNonce(). + @GuardedBy("mLock") + private void throwIfBadHandle(int handle) { + if (handle < 0 || handle > mHighestIndex) { + throw new IllegalArgumentException("invalid nonce handle: " + handle); + } + } + + // Throw if the memory is immutable (the process does not have write permission). The + // exception mimics the permission-denied exception thrown when a process writes to an + // unauthorized system property. + private void throwIfImmutable() { + if (!mMutable) { + throw new RuntimeException("write permission denied"); + } + } + + // Add a string to the local copy of the block and write the block to shared memory. + // Return the index of the new string. If the string has already been recorded, the + // shared memory is not updated but the index of the existing string is returned. + public int storeName(@NonNull String str) { + synchronized (mLock) { + Integer handle = mStringHandle.get(str); + if (handle == null) { + throwIfImmutable(); + throwIfBadString(str); + appendStringToMapLocked(str); + nativeSetByteBlock(mPtr, mBlockHash, mStringBlock); + updateStringMapLocked(); + handle = mStringHandle.get(str); + } + return handle; + } + } + + // Retrieve the handle for a string. -1 is returned if the string is not found. + public int getHandleForName(@NonNull String str) { + synchronized (mLock) { + Integer handle = mStringHandle.get(str); + if (handle == null) { + refreshStringBlockLocked(); + handle = mStringHandle.get(str); + } + return (handle != null) ? handle : INVALID_NONCE_INDEX; + } + } + + // Thin wrapper around the native method. + public boolean setNonce(int handle, long value) { + synchronized (mLock) { + throwIfBadHandle(handle); + throwIfImmutable(); + return nativeSetNonce(mPtr, handle, value); + } + } + + public long getNonce(int handle) { + synchronized (mLock) { + throwIfBadHandle(handle); + return nativeGetNonce(mPtr, handle); + } + } + + /** + * Dump the nonce statistics + */ + public void dump(@NonNull PrintWriter pw, @NonNull String prefix, boolean detailed) { + synchronized (mLock) { + pw.println(formatSimple( + "%sStringsMapped: %d, BytesUsed: %d", + prefix, mHighestIndex, mStringBytes)); + pw.println(formatSimple( + "%sPartialReads: %d, StringUpdates: %d", + prefix, mPartialReads, mStringUpdated)); + + if (detailed) { + for (String s: mStringHandle.keySet()) { + int h = mStringHandle.get(s); + pw.println(formatSimple( + "%sHandle:%d Name:%s", prefix, h, s)); + } + } + } + } + } + + /** + * Return the maximum number of nonces supported in the native layer. + * + * @param mPtr the pointer to the native shared memory. + * @return the number of nonces supported by the shared memory. + */ + private static native int nativeGetMaxNonce(long mPtr); + + /** + * Return the maximum number of string bytes supported in the native layer. + * + * @param mPtr the pointer to the native shared memory. + * @return the number of string bytes supported by the shared memory. + */ + private static native int nativeGetMaxByte(long mPtr); + + /** + * Write the byte block and set the hash into shared memory. The method is relatively + * forgiving, in that any non-null byte array will be stored without error. The number of + * bytes will the lesser of the length of the block parameter and the size of the native + * array. The native layer performs no checks on either byte block or the hash. + * + * @param mPtr the pointer to the native shared memory. + * @param hash a value to be stored in the native block hash. + * @param block the byte array to be store. + */ + @FastNative + private static native void nativeSetByteBlock(long mPtr, int hash, @NonNull byte[] block); + + /** + * Retrieve the string block into the array and return the hash value. If the incoming hash + * value is the same as the hash in shared memory, the native function returns immediately + * without touching the block parameter. Note that a zero hash value will always cause shared + * memory to be read. The number of bytes read is the lesser of the length of the block + * parameter and the size of the native array. + * + * @param mPtr the pointer to the native shared memory. + * @param hash a value to be compared against the hash in the native layer. + * @param block an array to receive the bytes from the native layer. + * @return the hash from the native layer. + */ + @FastNative + private static native int nativeGetByteBlock(long mPtr, int hash, @NonNull byte[] block); + + /** + * Retrieve just the byte block hash from the native layer. The function is CriticalNative + * and thus very fast. + * + * @param mPtr the pointer to the native shared memory. + * @return the current native hash value. + */ + @CriticalNative + private static native int nativeGetByteBlockHash(long mPtr); + + /** + * Set a nonce at the specified index. The index is checked against the size of the native + * nonce array and the function returns true if the index is valid, and false. The function + * is CriticalNative and thus very fast. + * + * @param mPtr the pointer to the native shared memory. + * @param index the index of the nonce to set. + * @param value the value to set for the nonce. + * @return true if the index is inside the nonce array and false otherwise. + */ + @CriticalNative + private static native boolean nativeSetNonce(long mPtr, int index, long value); + + /** + * Get the nonce from the specified index. The index is checked against the size of the + * native nonce array; the function returns the nonce value if the index is valid, and 0 + * otherwise. The function is CriticalNative and thus very fast. + * + * @param mPtr the pointer to the native shared memory. + * @param index the index of the nonce to retrieve. + * @return the value of the specified nonce, of 0 if the index is out of bounds. + */ + @CriticalNative + private static native long nativeGetNonce(long mPtr, int index); } diff --git a/core/java/android/app/performance.aconfig b/core/java/android/app/performance.aconfig new file mode 100644 index 000000000000..7c6989e4f3e9 --- /dev/null +++ b/core/java/android/app/performance.aconfig @@ -0,0 +1,11 @@ +package: "android.app" +container: "system" + +flag { + namespace: "system_performance" + name: "pic_uses_shared_memory" + is_exported: true + is_fixed_read_only: true + description: "PropertyInvalidatedCache uses shared memory for nonces." + bug: "366552454" +} diff --git a/core/java/com/android/internal/os/ApplicationSharedMemory.java b/core/java/com/android/internal/os/ApplicationSharedMemory.java index 84f713edcc1a..e6ea29e483f1 100644 --- a/core/java/com/android/internal/os/ApplicationSharedMemory.java +++ b/core/java/com/android/internal/os/ApplicationSharedMemory.java @@ -21,6 +21,7 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import dalvik.annotation.optimization.CriticalNative; +import dalvik.annotation.optimization.FastNative; import libcore.io.IoUtils; @@ -293,4 +294,34 @@ public class ApplicationSharedMemory implements AutoCloseable { throw new IllegalStateException("Not mutable"); } } + + /** + * Return true if the memory has been mapped. This never throws. + */ + public boolean isMapped() { + return mPtr != 0; + } + + /** + * Return true if the memory is mapped and mutable. This never throws. Note that it returns + * false if the memory is not mapped. + */ + public boolean isMutable() { + return isMapped() && mMutable; + } + + /** + * Provide access to the nonce block needed by {@link PropertyInvalidatedCache}. This method + * returns 0 if the shared memory is not (yet) mapped. + */ + public long getSystemNonceBlock() { + return isMapped() ? nativeGetSystemNonceBlock(mPtr) : 0; + } + + /** + * Return a pointer to the system nonce cache in the shared memory region. The method is + * idempotent. + */ + @FastNative + private static native long nativeGetSystemNonceBlock(long ptr); } diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 816ace2310a5..eb07f7c125d0 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -249,6 +249,7 @@ cc_library_shared_for_libandroid_runtime { "android_backup_BackupDataOutput.cpp", "android_backup_FileBackupHelperBase.cpp", "android_backup_BackupHelperDispatcher.cpp", + "android_app_PropertyInvalidatedCache.cpp", "android_app_backup_FullBackup.cpp", "android_content_res_ApkAssets.cpp", "android_content_res_ObbScanner.cpp", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 76f66cd4ebc9..821861efd59b 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -177,6 +177,7 @@ extern int register_android_app_backup_FullBackup(JNIEnv *env); extern int register_android_app_Activity(JNIEnv *env); extern int register_android_app_ActivityThread(JNIEnv *env); extern int register_android_app_NativeActivity(JNIEnv *env); +extern int register_android_app_PropertyInvalidatedCache(JNIEnv* env); extern int register_android_media_RemoteDisplay(JNIEnv *env); extern int register_android_util_jar_StrictJarFile(JNIEnv* env); extern int register_android_view_InputChannel(JNIEnv* env); @@ -1659,6 +1660,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_app_Activity), REG_JNI(register_android_app_ActivityThread), REG_JNI(register_android_app_NativeActivity), + REG_JNI(register_android_app_PropertyInvalidatedCache), REG_JNI(register_android_util_jar_StrictJarFile), REG_JNI(register_android_view_InputChannel), REG_JNI(register_android_view_InputEventReceiver), diff --git a/core/jni/android_app_PropertyInvalidatedCache.cpp b/core/jni/android_app_PropertyInvalidatedCache.cpp new file mode 100644 index 000000000000..ead66660a0a4 --- /dev/null +++ b/core/jni/android_app_PropertyInvalidatedCache.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 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. + */ + +#define LOG_TAG "CacheNonce" + +#include <string.h> +#include <memory.h> + +#include <atomic> + +#include <nativehelper/JNIHelp.h> +#include <nativehelper/scoped_primitive_array.h> +#include <android-base/logging.h> + +#include "core_jni_helpers.h" +#include "android_app_PropertyInvalidatedCache.h" + +namespace { + +using namespace android::app::PropertyInvalidatedCache; + +// Convert a jlong to a nonce block. This is a convenience function that should be inlined by +// the compiler. +inline SystemCacheNonce* sysCache(jlong ptr) { + return reinterpret_cast<SystemCacheNonce*>(ptr); +} + +// Return the number of nonces in the nonce block. +jint getMaxNonce(JNIEnv*, jclass, jlong ptr) { + return sysCache(ptr)->getMaxNonce(); +} + +// Return the number of string bytes in the nonce block. +jint getMaxByte(JNIEnv*, jclass, jlong ptr) { + return sysCache(ptr)->getMaxByte(); +} + +// Set the byte block. The first int is the hash to set and the second is the array to copy. +// This should be synchronized in the Java layer. +void setByteBlock(JNIEnv* env, jclass, jlong ptr, jint hash, jbyteArray val) { + ScopedByteArrayRO value(env, val); + if (value.get() == nullptr) { + jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", "null byte block"); + return; + } + sysCache(ptr)->setByteBlock(hash, value.get(), value.size()); +} + +// Fetch the byte block. If the incoming hash is the same as the local hash, the Java layer is +// presumed to have an up-to-date copy of the byte block; do not copy byte array. The local +// hash is returned. +jint getByteBlock(JNIEnv* env, jclass, jlong ptr, jint hash, jbyteArray val) { + if (sysCache(ptr)->getHash() == hash) { + return hash; + } + ScopedByteArrayRW value(env, val); + return sysCache(ptr)->getByteBlock(value.get(), value.size()); +} + +// Fetch the byte block hash. +// +// This is a CriticalNative method and therefore does not get the JNIEnv or jclass parameters. +jint getByteBlockHash(jlong ptr) { + return sysCache(ptr)->getHash(); +} + +// Get a nonce value. So that this method can be CriticalNative, it returns 0 if the value is +// out of range, rather than throwing an exception. This is a CriticalNative method and +// therefore does not get the JNIEnv or jclass parameters. +// +// This method is @CriticalNative and does not take a JNIEnv* or jclass argument. +jlong getNonce(jlong ptr, jint index) { + return sysCache(ptr)->getNonce(index); +} + +// Set a nonce value. So that this method can be CriticalNative, it returns a boolean: false if +// the index is out of range and true otherwise. Callers may test the returned boolean and +// generate an exception. +// +// This method is @CriticalNative and does not take a JNIEnv* or jclass argument. +jboolean setNonce(jlong ptr, jint index, jlong value) { + return sysCache(ptr)->setNonce(index, value); +} + +static const JNINativeMethod gMethods[] = { + {"nativeGetMaxNonce", "(J)I", (void*) getMaxNonce }, + {"nativeGetMaxByte", "(J)I", (void*) getMaxByte }, + {"nativeSetByteBlock", "(JI[B)V", (void*) setByteBlock }, + {"nativeGetByteBlock", "(JI[B)I", (void*) getByteBlock }, + {"nativeGetByteBlockHash", "(J)I", (void*) getByteBlockHash }, + {"nativeGetNonce", "(JI)J", (void*) getNonce }, + {"nativeSetNonce", "(JIJ)Z", (void*) setNonce }, +}; + +static const char* kClassName = "android/app/PropertyInvalidatedCache"; + +} // anonymous namespace + +namespace android { + +int register_android_app_PropertyInvalidatedCache(JNIEnv* env) { + RegisterMethodsOrDie(env, kClassName, gMethods, NELEM(gMethods)); + return JNI_OK; +} + +} // namespace android diff --git a/core/jni/android_app_PropertyInvalidatedCache.h b/core/jni/android_app_PropertyInvalidatedCache.h new file mode 100644 index 000000000000..eefa8fa88624 --- /dev/null +++ b/core/jni/android_app_PropertyInvalidatedCache.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 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. + */ + +#include <string.h> +#include <memory.h> + +#include <atomic> + +namespace android { +namespace app { +namespace PropertyInvalidatedCache { + +/** + * A cache nonce block contains an array of std::atomic<int64_t> and an array of bytes. The + * byte array has an associated hash. This class provides methods to read and write the fields + * of the block but it does not interpret the fields. + * + * On initialization, all fields are set to zero. + * + * In general, methods do not report errors. This allows the methods to be used in + * CriticalNative JNI APIs. + * + * The template is parameterized by the number of nonces it supports and the number of bytes in + * the string block. + */ +template<int maxNonce, size_t maxByte> class CacheNonce { + + // The value of an unset field. + static const int UNSET = 0; + + // A convenient typedef. The jbyteArray element type is jbyte, which the compiler treats as + // signed char. + typedef signed char block_t; + + // The array of nonces + volatile std::atomic<int64_t> mNonce[maxNonce]; + + // The byte array. This is not atomic but it is guarded by the mByteHash. + volatile block_t mByteBlock[maxByte]; + + // The hash that validates the byte block + volatile std::atomic<int32_t> mByteHash; + + // Pad the class to a multiple of 8 bytes. + int32_t _pad; + + public: + + // The expected size of this instance. This is a compile-time constant and can be used in a + // static assertion. + static const int expectedSize = + maxNonce * sizeof(std::atomic<int64_t>) + + sizeof(std::atomic<int32_t>) + + maxByte * sizeof(block_t) + + sizeof(int32_t); + + // These provide run-time access to the sizing parameters. + int getMaxNonce() const { + return maxNonce; + } + + size_t getMaxByte() const { + return maxByte; + } + + // Construct and initialize the memory. + CacheNonce() { + for (int i = 0; i < maxNonce; i++) { + mNonce[i] = UNSET; + } + mByteHash = UNSET; + memset((void*) mByteBlock, UNSET, sizeof(mByteBlock)); + } + + // Fetch a nonce, returning UNSET if the index is out of range. This method specifically + // does not throw or generate an error if the index is out of range; this allows the method + // to be called in a CriticalNative JNI API. + int64_t getNonce(int index) const { + if (index < 0 || index >= maxNonce) { + return UNSET; + } else { + return mNonce[index]; + } + } + + // Set a nonce and return true. Return false if the index is out of range. This method + // specifically does not throw or generate an error if the index is out of range; this + // allows the method to be called in a CriticalNative JNI API. + bool setNonce(int index, int64_t value) { + if (index < 0 || index >= maxNonce) { + return false; + } else { + mNonce[index] = value; + return true; + } + } + + // Fetch just the byte-block hash + int32_t getHash() const { + return mByteHash; + } + + // Copy the byte block to the target and return the current hash. + int32_t getByteBlock(block_t* block, size_t len) const { + memcpy(block, (void*) mByteBlock, std::min(maxByte, len)); + return mByteHash; + } + + // Set the byte block and the hash. + void setByteBlock(int hash, const block_t* block, size_t len) { + memcpy((void*) mByteBlock, block, len = std::min(maxByte, len)); + mByteHash = hash; + } +}; + +/** + * Sizing parameters for the system_server PropertyInvalidatedCache support. A client can + * retrieve the values through the accessors in CacheNonce instances. + */ +static const int MAX_NONCE = 64; +static const int BYTE_BLOCK_SIZE = 8192; + +// The CacheNonce for system server holds 64 nonces with a string block of 8192 bytes. +typedef CacheNonce<MAX_NONCE, BYTE_BLOCK_SIZE> SystemCacheNonce; + +// The goal of this assertion is to ensure that the data structure is the same size across 32-bit +// and 64-bit systems. +static_assert(sizeof(SystemCacheNonce) == SystemCacheNonce::expectedSize, + "Unexpected SystemCacheNonce size"); + +} // namespace PropertyInvalidatedCache +} // namespace app +} // namespace android diff --git a/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp b/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp index 453e53974e0d..cc1687cd9ffb 100644 --- a/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp +++ b/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp @@ -29,8 +29,12 @@ #include "core_jni_helpers.h" +#include "android_app_PropertyInvalidatedCache.h" + namespace { +using namespace android::app::PropertyInvalidatedCache; + // Atomics should be safe to use across processes if they are lock free. static_assert(std::atomic<int64_t>::is_always_lock_free == true, "atomic<int64_t> is not always lock free"); @@ -64,12 +68,15 @@ public: void setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(int64_t offset) { latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis = offset; } + + // The nonce storage for pic. The sizing is suitable for the system server module. + SystemCacheNonce systemPic; }; // Update the expected value when modifying the members of SharedMemory. // The goal of this assertion is to ensure that the data structure is the same size across 32-bit // and 64-bit systems. -static_assert(sizeof(SharedMemory) == 8, "Unexpected SharedMemory size"); +static_assert(sizeof(SharedMemory) == 8 + sizeof(SystemCacheNonce), "Unexpected SharedMemory size"); static jint nativeCreate(JNIEnv* env, jclass) { // Create anonymous shared memory region @@ -133,6 +140,12 @@ static jlong nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMilli return sharedMemory->getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(); } +// This is a FastNative method. It takes the usual JNIEnv* and jclass* arguments. +static jlong nativeGetSystemNonceBlock(JNIEnv*, jclass*, jlong ptr) { + SharedMemory* sharedMemory = reinterpret_cast<SharedMemory*>(ptr); + return reinterpret_cast<jlong>(&sharedMemory->systemPic); +} + static const JNINativeMethod gMethods[] = { {"nativeCreate", "()I", (void*)nativeCreate}, {"nativeMap", "(IZ)J", (void*)nativeMap}, @@ -143,16 +156,17 @@ static const JNINativeMethod gMethods[] = { (void*)nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis}, {"nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis", "(J)J", (void*)nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis}, + {"nativeGetSystemNonceBlock", "(J)J", (void*) nativeGetSystemNonceBlock}, }; -} // anonymous namespace - -namespace android { - static const char kApplicationSharedMemoryClassName[] = "com/android/internal/os/ApplicationSharedMemory"; static jclass gApplicationSharedMemoryClass; +} // anonymous namespace + +namespace android { + int register_com_android_internal_os_ApplicationSharedMemory(JNIEnv* env) { gApplicationSharedMemoryClass = MakeGlobalRefOrDie(env, FindClassOrDie(env, kApplicationSharedMemoryClassName)); diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index dcea5b299829..65153f55295a 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -16,13 +16,23 @@ package android.app; +import static android.app.PropertyInvalidatedCache.NONCE_UNSET; +import static android.app.PropertyInvalidatedCache.NonceStore.INVALID_NONCE_INDEX; +import static com.android.internal.os.Flags.FLAG_APPLICATION_SHARED_MEMORY_ENABLED; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.android.internal.os.ApplicationSharedMemory; + import android.platform.test.annotations.IgnoreUnderRavenwood; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; @@ -47,6 +57,9 @@ public class PropertyInvalidatedCacheTests { @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + // Configuration for creating caches private static final String MODULE = PropertyInvalidatedCache.MODULE_TEST; private static final String API = "testApi"; @@ -423,4 +436,54 @@ public class PropertyInvalidatedCacheTests { // Re-enable test mode (so that the cleanup for the test does not throw). PropertyInvalidatedCache.setTestMode(true); } + + // Verify the behavior of shared memory nonce storage. This does not directly test the cache + // storing nonces in shared memory. + @RequiresFlagsEnabled(FLAG_APPLICATION_SHARED_MEMORY_ENABLED) + @Test + public void testSharedMemoryStorage() { + // Fetch a shared memory instance for testing. + ApplicationSharedMemory shmem = ApplicationSharedMemory.create(); + + // Create a server-side store and a client-side store. The server's store is mutable and + // the client's store is not mutable. + PropertyInvalidatedCache.NonceStore server = + new PropertyInvalidatedCache.NonceStore(shmem.getSystemNonceBlock(), true); + PropertyInvalidatedCache.NonceStore client = + new PropertyInvalidatedCache.NonceStore(shmem.getSystemNonceBlock(), false); + + final String name1 = "name1"; + assertEquals(server.getHandleForName(name1), INVALID_NONCE_INDEX); + assertEquals(client.getHandleForName(name1), INVALID_NONCE_INDEX); + final int index1 = server.storeName(name1); + assertNotEquals(index1, INVALID_NONCE_INDEX); + assertEquals(server.getHandleForName(name1), index1); + assertEquals(client.getHandleForName(name1), index1); + assertEquals(server.storeName(name1), index1); + + assertEquals(server.getNonce(index1), NONCE_UNSET); + assertEquals(client.getNonce(index1), NONCE_UNSET); + final int value1 = 4; + server.setNonce(index1, value1); + assertEquals(server.getNonce(index1), value1); + assertEquals(client.getNonce(index1), value1); + final int value2 = 8; + server.setNonce(index1, value2); + assertEquals(server.getNonce(index1), value2); + assertEquals(client.getNonce(index1), value2); + + final String name2 = "name2"; + assertEquals(server.getHandleForName(name2), INVALID_NONCE_INDEX); + assertEquals(client.getHandleForName(name2), INVALID_NONCE_INDEX); + final int index2 = server.storeName(name2); + assertNotEquals(index2, INVALID_NONCE_INDEX); + assertEquals(server.getHandleForName(name2), index2); + assertEquals(client.getHandleForName(name2), index2); + assertEquals(server.storeName(name2), index2); + + // The names are different, so the indices must be different. + assertNotEquals(index1, index2); + + shmem.close(); + } } |