diff options
author | 2024-11-08 12:42:45 -0800 | |
---|---|---|
committer | 2024-12-04 09:50:35 -0800 | |
commit | d857b66266cc077810d438bba8e2d61b28ecfb2e (patch) | |
tree | 8db865ec39f170124c19a910050276082ae8d695 | |
parent | 62ebefc207650117c7688c99195884dae8224566 (diff) |
Support target process in dynamic instrumentation
Make DynamicInstrumentationManagerService talk to an app process to
obtain method offsets when the target process is an app. This is done
via calling a new AIDL method
IApplicationThread.getExecutableMethodFileOffsets. The implementation
(ActivityThread) runs in an app process and calls ART VMDebug in-process
to get the offsets.
The offset results are now provided asynchronously both from app to
system_server, and from system_server to uprobestats. This way
system_server would not block on an app process.
Bug: 372925025
Flag: com.android.art.flags.executable_method_file_offsets
Change-Id: Ie58a21d12530000b858aa292a406755a158b4c4a
Test: atest ExecutableMethodFileOffsetsTest
10 files changed, 269 insertions, 77 deletions
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index d9f8d33f0545..6c6e28fa1623 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -44,6 +44,8 @@ import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; import android.os.TransactionTooLargeException; import android.os.WorkSource; +import android.os.instrumentation.IOffsetCallback; +import android.os.instrumentation.MethodDescriptor; import android.util.ArraySet; import android.util.Pair; @@ -1339,6 +1341,14 @@ public abstract class ActivityManagerInternal { String reason, int exitInfoReason); /** + * Queries the offset data for a given method on a process. + * @hide + */ + public abstract void getExecutableMethodFileOffsets(@NonNull String processName, + int pid, int uid, @NonNull MethodDescriptor methodDescriptor, + @NonNull IOffsetCallback callback); + + /** * Add a creator token for all embedded intents (stored as extra) of the given intent. * * @param intent The given intent diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index cb7b1153988a..fdde02d289bb 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -165,6 +165,10 @@ import android.os.TelephonyServiceManager; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; +import android.os.instrumentation.ExecutableMethodFileOffsets; +import android.os.instrumentation.IOffsetCallback; +import android.os.instrumentation.MethodDescriptor; +import android.os.instrumentation.MethodDescriptorParser; import android.permission.IPermissionManager; import android.provider.BlockedNumberContract; import android.provider.CalendarContract; @@ -2225,6 +2229,29 @@ public final class ActivityThread extends ClientTransactionHandler args.arg6 = uiTranslationSpec; sendMessage(H.UPDATE_UI_TRANSLATION_STATE, args); } + + @Override + public void getExecutableMethodFileOffsets( + @NonNull MethodDescriptor methodDescriptor, + @NonNull IOffsetCallback resultCallback) { + Method method = MethodDescriptorParser.parseMethodDescriptor( + getClass().getClassLoader(), methodDescriptor); + VMDebug.ExecutableMethodFileOffsets location = + VMDebug.getExecutableMethodFileOffsets(method); + try { + if (location == null) { + resultCallback.onResult(null); + return; + } + ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets(); + ret.containerPath = location.getContainerPath(); + ret.containerOffset = location.getContainerOffset(); + ret.methodOffset = location.getMethodOffset(); + resultCallback.onResult(ret); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } private @NonNull SafeCancellationTransport createSafeCancellationTransport( diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl index 06d01ecfcf06..063501bf82a2 100644 --- a/core/java/android/app/IApplicationThread.aidl +++ b/core/java/android/app/IApplicationThread.aidl @@ -46,6 +46,8 @@ import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.SharedMemory; +import android.os.instrumentation.IOffsetCallback; +import android.os.instrumentation.MethodDescriptor; import android.view.autofill.AutofillId; import android.view.translation.TranslationSpec; import android.view.translation.UiTranslationSpec; @@ -183,4 +185,6 @@ oneway interface IApplicationThread { void scheduleTimeoutService(IBinder token, int startId); void scheduleTimeoutServiceForType(IBinder token, int startId, int fgsType); void schedulePing(in RemoteCallback pong); + void getExecutableMethodFileOffsets(in MethodDescriptor methodDescriptor, + in IOffsetCallback resultCallback); } diff --git a/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl index c45c51d15cc9..af56bfe50381 100644 --- a/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl +++ b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl @@ -16,7 +16,7 @@ package android.os.instrumentation; -import android.os.instrumentation.ExecutableMethodFileOffsets; +import android.os.instrumentation.IOffsetCallback; import android.os.instrumentation.MethodDescriptor; import android.os.instrumentation.TargetProcess; @@ -28,6 +28,7 @@ import android.os.instrumentation.TargetProcess; interface IDynamicInstrumentationManager { /** Provides ART metadata about the described compiled method within the target process */ @PermissionManuallyEnforced - @nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets( - in TargetProcess targetProcess, in MethodDescriptor methodDescriptor); + void getExecutableMethodFileOffsets( + in TargetProcess targetProcess, in MethodDescriptor methodDescriptor, + in IOffsetCallback callback); } diff --git a/core/java/android/os/instrumentation/IOffsetCallback.aidl b/core/java/android/os/instrumentation/IOffsetCallback.aidl new file mode 100644 index 000000000000..a28c93f5353a --- /dev/null +++ b/core/java/android/os/instrumentation/IOffsetCallback.aidl @@ -0,0 +1,28 @@ +/* + * 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. + */ + +package android.os.instrumentation; + +import android.os.instrumentation.ExecutableMethodFileOffsets; + +/** + * System private API for providing dynamic instrumentation offset results. + * + * {@hide} + */ +oneway interface IOffsetCallback { + void onResult(in @nullable ExecutableMethodFileOffsets offsets); +} diff --git a/core/java/android/os/instrumentation/MethodDescriptorParser.java b/core/java/android/os/instrumentation/MethodDescriptorParser.java new file mode 100644 index 000000000000..57fc44ff623e --- /dev/null +++ b/core/java/android/os/instrumentation/MethodDescriptorParser.java @@ -0,0 +1,82 @@ +/* + * 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. + */ + +package android.os.instrumentation; + +import android.annotation.NonNull; + +import java.lang.reflect.Method; + +/** + * A utility class for dynamic instrumentation / uprobestats. + * + * @hide + */ +public final class MethodDescriptorParser { + + /** + * Parses a {@link MethodDescriptor} (in string representation) into a {@link Method}. + */ + public static Method parseMethodDescriptor(ClassLoader classLoader, + @NonNull MethodDescriptor descriptor) { + try { + Class<?> javaClass = classLoader.loadClass(descriptor.fullyQualifiedClassName); + Class<?>[] parameters = new Class[descriptor.fullyQualifiedParameters.length]; + for (int i = 0; i < descriptor.fullyQualifiedParameters.length; i++) { + String typeName = descriptor.fullyQualifiedParameters[i]; + boolean isArrayType = typeName.endsWith("[]"); + if (isArrayType) { + typeName = typeName.substring(0, typeName.length() - 2); + } + switch (typeName) { + case "boolean": + parameters[i] = isArrayType ? boolean.class.arrayType() : boolean.class; + break; + case "byte": + parameters[i] = isArrayType ? byte.class.arrayType() : byte.class; + break; + case "char": + parameters[i] = isArrayType ? char.class.arrayType() : char.class; + break; + case "short": + parameters[i] = isArrayType ? short.class.arrayType() : short.class; + break; + case "int": + parameters[i] = isArrayType ? int.class.arrayType() : int.class; + break; + case "long": + parameters[i] = isArrayType ? long.class.arrayType() : long.class; + break; + case "float": + parameters[i] = isArrayType ? float.class.arrayType() : float.class; + break; + case "double": + parameters[i] = isArrayType ? double.class.arrayType() : double.class; + break; + default: + parameters[i] = isArrayType ? classLoader.loadClass(typeName).arrayType() + : classLoader.loadClass(typeName); + } + } + + return javaClass.getDeclaredMethod(descriptor.methodName, parameters); + } catch (ClassNotFoundException | NoSuchMethodException e) { + throw new IllegalArgumentException( + "The specified method cannot be found. Is this descriptor valid? " + + descriptor, e); + } + } +} diff --git a/native/android/dynamic_instrumentation_manager.cpp b/native/android/dynamic_instrumentation_manager.cpp index 532213611cf1..074973188c66 100644 --- a/native/android/dynamic_instrumentation_manager.cpp +++ b/native/android/dynamic_instrumentation_manager.cpp @@ -15,7 +15,9 @@ */ #define LOG_TAG "ADynamicInstrumentationManager" +#include <android-base/properties.h> #include <android/dynamic_instrumentation_manager.h> +#include <android/os/instrumentation/BnOffsetCallback.h> #include <android/os/instrumentation/ExecutableMethodFileOffsets.h> #include <android/os/instrumentation/IDynamicInstrumentationManager.h> #include <android/os/instrumentation/MethodDescriptor.h> @@ -23,7 +25,9 @@ #include <binder/Binder.h> #include <binder/IServiceManager.h> #include <utils/Log.h> +#include <utils/StrongPointer.h> +#include <future> #include <mutex> #include <optional> #include <string> @@ -31,6 +35,9 @@ namespace android::dynamicinstrumentationmanager { +using android::os::instrumentation::BnOffsetCallback; +using android::os::instrumentation::ExecutableMethodFileOffsets; + // Global instance of IDynamicInstrumentationManager, service is obtained only on first use. static std::mutex mLock; static sp<os::instrumentation::IDynamicInstrumentationManager> mService; @@ -131,6 +138,30 @@ void ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy( delete instance; } +class ResultCallback : public BnOffsetCallback { +public: + ::android::binder::Status onResult( + const ::std::optional<ExecutableMethodFileOffsets>& offsets) override { + promise_.set_value(offsets); + return android::binder::Status::ok(); + } + + std::optional<ExecutableMethodFileOffsets> waitForResult() { + std::future<std::optional<ExecutableMethodFileOffsets>> futureResult = + promise_.get_future(); + auto futureStatus = futureResult.wait_for( + std::chrono::seconds(1 * android::base::HwTimeoutMultiplier())); + if (futureStatus == std::future_status::ready) { + return futureResult.get(); + } else { + return std::nullopt; + } + } + +private: + std::promise<std::optional<ExecutableMethodFileOffsets>> promise_; +}; + int32_t ADynamicInstrumentationManager_getExecutableMethodFileOffsets( const ADynamicInstrumentationManager_TargetProcess* targetProcess, const ADynamicInstrumentationManager_MethodDescriptor* methodDescriptor, @@ -150,15 +181,15 @@ int32_t ADynamicInstrumentationManager_getExecutableMethodFileOffsets( return INVALID_OPERATION; } - std::optional<android::os::instrumentation::ExecutableMethodFileOffsets> offsets; + android::sp<ResultCallback> resultCallback = android::sp<ResultCallback>::make(); binder_status_t result = service->getExecutableMethodFileOffsets(targetProcessParcel, methodDescriptorParcel, - &offsets) + resultCallback) .exceptionCode(); if (result != OK) { return result; } - + std::optional<ExecutableMethodFileOffsets> offsets = resultCallback->waitForResult(); if (offsets != std::nullopt) { auto* value = new ADynamicInstrumentationManager_ExecutableMethodFileOffsets(); value->containerPath = offsets->containerPath; @@ -170,4 +201,4 @@ int32_t ADynamicInstrumentationManager_getExecutableMethodFileOffsets( } return result; -}
\ No newline at end of file +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 5c5361bacf5a..b2168ce100bf 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -357,6 +357,8 @@ import android.os.WorkSource; import android.os.incremental.IIncrementalService; import android.os.incremental.IncrementalManager; import android.os.incremental.IncrementalMetrics; +import android.os.instrumentation.IOffsetCallback; +import android.os.instrumentation.MethodDescriptor; import android.os.storage.IStorageManager; import android.os.storage.StorageManager; import android.provider.DeviceConfig; @@ -478,6 +480,7 @@ import com.android.server.wm.WindowProcessController; import dalvik.annotation.optimization.NeverCompile; import dalvik.system.VMRuntime; + import libcore.util.EmptyArray; import java.io.File; @@ -502,6 +505,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -18036,6 +18040,26 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public void getExecutableMethodFileOffsets(@NonNull String processName, + int pid, int uid, @NonNull MethodDescriptor methodDescriptor, + @NonNull IOffsetCallback callback) { + final IApplicationThread thread; + synchronized (ActivityManagerService.this) { + ProcessRecord record = mProcessList.getProcessRecordLocked(processName, uid); + if (record == null || record.getPid() != pid) { + throw new NoSuchElementException(); + } + thread = record.getThread(); + } + try { + thread.getExecutableMethodFileOffsets(methodDescriptor, callback); + } catch (RemoteException e) { + throw new RuntimeException( + "IApplicationThread.getExecutableMethodFileOffsets failed", e); + } + } + + @Override public void addCreatorToken(Intent intent, String creatorPackage) { ActivityManagerService.this.addCreatorToken(intent, creatorPackage); } diff --git a/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java index 8ec716077f46..871d12ee6394 100644 --- a/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java +++ b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java @@ -20,116 +20,100 @@ import static android.Manifest.permission.DYNAMIC_INSTRUMENTATION; import static android.content.Context.DYNAMIC_INSTRUMENTATION_SERVICE; import android.annotation.NonNull; -import android.annotation.Nullable; import android.annotation.PermissionManuallyEnforced; +import android.annotation.RequiresPermission; +import android.app.ActivityManagerInternal; import android.content.Context; +import android.os.RemoteException; import android.os.instrumentation.ExecutableMethodFileOffsets; import android.os.instrumentation.IDynamicInstrumentationManager; +import android.os.instrumentation.IOffsetCallback; import android.os.instrumentation.MethodDescriptor; +import android.os.instrumentation.MethodDescriptorParser; import android.os.instrumentation.TargetProcess; -import com.android.internal.annotations.VisibleForTesting; + +import com.android.server.LocalServices; import com.android.server.SystemService; import dalvik.system.VMDebug; import java.lang.reflect.Method; +import java.util.NoSuchElementException; +import java.util.Objects; + /** * System private implementation of the {@link IDynamicInstrumentationManager interface}. */ public class DynamicInstrumentationManagerService extends SystemService { + + private ActivityManagerInternal mAmInternal; + public DynamicInstrumentationManagerService(@NonNull Context context) { super(context); } @Override public void onStart() { + mAmInternal = LocalServices.getService(ActivityManagerInternal.class); publishBinderService(DYNAMIC_INSTRUMENTATION_SERVICE, new BinderService()); } private final class BinderService extends IDynamicInstrumentationManager.Stub { @Override @PermissionManuallyEnforced - public @Nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets( - @NonNull TargetProcess targetProcess, @NonNull MethodDescriptor methodDescriptor) { + @RequiresPermission(value = android.Manifest.permission.DYNAMIC_INSTRUMENTATION) + public void getExecutableMethodFileOffsets( + @NonNull TargetProcess targetProcess, @NonNull MethodDescriptor methodDescriptor, + @NonNull IOffsetCallback callback) { if (!com.android.art.flags.Flags.executableMethodFileOffsets()) { throw new UnsupportedOperationException(); } getContext().enforceCallingOrSelfPermission( DYNAMIC_INSTRUMENTATION, "Caller must have DYNAMIC_INSTRUMENTATION permission"); + Objects.requireNonNull(targetProcess.processName); - if (targetProcess.processName == null - || !targetProcess.processName.equals("system_server")) { - throw new UnsupportedOperationException( - "system_server is the only supported target process"); + if (!targetProcess.processName.equals("system_server")) { + try { + mAmInternal.getExecutableMethodFileOffsets(targetProcess.processName, + targetProcess.pid, targetProcess.uid, methodDescriptor, + new IOffsetCallback.Stub() { + @Override + public void onResult(ExecutableMethodFileOffsets result) { + try { + callback.onResult(result); + } catch (RemoteException e) { + /* ignore */ + } + } + }); + return; + } catch (NoSuchElementException e) { + throw new IllegalArgumentException( + "The specified app process cannot be found." , e); + } } - Method method = parseMethodDescriptor( + Method method = MethodDescriptorParser.parseMethodDescriptor( getClass().getClassLoader(), methodDescriptor); VMDebug.ExecutableMethodFileOffsets location = VMDebug.getExecutableMethodFileOffsets(method); - if (location == null) { - return null; - } - - ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets(); - ret.containerPath = location.getContainerPath(); - ret.containerOffset = location.getContainerOffset(); - ret.methodOffset = location.getMethodOffset(); - return ret; - } - } - - @VisibleForTesting - static Method parseMethodDescriptor(ClassLoader classLoader, - @NonNull MethodDescriptor descriptor) { - try { - Class<?> javaClass = classLoader.loadClass(descriptor.fullyQualifiedClassName); - Class<?>[] parameters = new Class[descriptor.fullyQualifiedParameters.length]; - for (int i = 0; i < descriptor.fullyQualifiedParameters.length; i++) { - String typeName = descriptor.fullyQualifiedParameters[i]; - boolean isArrayType = typeName.endsWith("[]"); - if (isArrayType) { - typeName = typeName.substring(0, typeName.length() - 2); + try { + if (location == null) { + callback.onResult(null); + return; } - switch (typeName) { - case "boolean": - parameters[i] = isArrayType ? boolean.class.arrayType() : boolean.class; - break; - case "byte": - parameters[i] = isArrayType ? byte.class.arrayType() : byte.class; - break; - case "char": - parameters[i] = isArrayType ? char.class.arrayType() : char.class; - break; - case "short": - parameters[i] = isArrayType ? short.class.arrayType() : short.class; - break; - case "int": - parameters[i] = isArrayType ? int.class.arrayType() : int.class; - break; - case "long": - parameters[i] = isArrayType ? long.class.arrayType() : long.class; - break; - case "float": - parameters[i] = isArrayType ? float.class.arrayType() : float.class; - break; - case "double": - parameters[i] = isArrayType ? double.class.arrayType() : double.class; - break; - default: - parameters[i] = isArrayType ? classLoader.loadClass(typeName).arrayType() - : classLoader.loadClass(typeName); - } - } - return javaClass.getDeclaredMethod(descriptor.methodName, parameters); - } catch (ClassNotFoundException | NoSuchMethodException e) { - throw new IllegalArgumentException( - "The specified method cannot be found. Is this descriptor valid? " - + descriptor, e); + ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets(); + ret.containerPath = location.getContainerPath(); + ret.containerOffset = location.getContainerOffset(); + ret.methodOffset = location.getMethodOffset(); + callback.onResult(ret); + } catch (RemoteException e) { + throw new RuntimeException("Failed to invoke result callback", e); + } } } } diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java index 5492ba6b9dd1..6e14bad11837 100644 --- a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java +++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import android.os.instrumentation.MethodDescriptor; +import android.os.instrumentation.MethodDescriptorParser; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; @@ -37,7 +38,7 @@ import java.lang.reflect.Method; /** * Test class for - * {@link DynamicInstrumentationManagerService#parseMethodDescriptor(ClassLoader, + * {@link MethodDescriptorParser#parseMethodDescriptor(ClassLoader, * MethodDescriptor)}. * <p> * Build/Install/Run: @@ -119,13 +120,13 @@ public class ParseMethodDescriptorTest { } private Method parseMethodDescriptor(String fqcn, String methodName) { - return DynamicInstrumentationManagerService.parseMethodDescriptor( + return MethodDescriptorParser.parseMethodDescriptor( getClass().getClassLoader(), getMethodDescriptor(fqcn, methodName, new String[]{})); } private Method parseMethodDescriptor(String fqcn, String methodName, String[] fqParameters) { - return DynamicInstrumentationManagerService.parseMethodDescriptor( + return MethodDescriptorParser.parseMethodDescriptor( getClass().getClassLoader(), getMethodDescriptor(fqcn, methodName, fqParameters)); } |