summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-15 09:36:19 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-15 09:36:19 +0000
commit3d936fdee35694f7baf0b52e4f6104168ca7aea6 (patch)
treedf27762550944b33bbb1eabf6e257d4f6d5e3b1d
parentcccf2c7a682045c0e26e5b95ef8400405f3a9854 (diff)
parent539a419cf93efa512a20c07c2a0e3fbf2bff8bc9 (diff)
downloadplatform_testing-aml_tz5_341510010.tar.gz
Snap for 11224086 from 539a419cf93efa512a20c07c2a0e3fbf2bff8bc9 to mainline-tzdata5-releaseaml_tz5_341510050aml_tz5_341510010aml_tz5_341510010
Change-Id: Ie62f8c6f1e007a94b4cfec3b3086ba557ed3b22a
-rw-r--r--build/tasks/continuous_instrumentation_tests.mk2
-rw-r--r--build/tasks/tests/instrumentation_test_list.mk3
-rw-r--r--build/tasks/tests/native_test_list.mk3
-rw-r--r--libraries/annotations/src/android/platform/test/annotations/IwTest.java35
-rw-r--r--libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor2.java68
-rw-r--r--libraries/app-helpers/interfaces/Android.bp1
-rw-r--r--libraries/app-helpers/interfaces/auto/OWNERS4
-rw-r--r--libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper2.java285
-rw-r--r--libraries/app-helpers/spectatio/spectatio-util/src/android/platform/spectatio/configs/validators/ValidateUiElement.java5
-rw-r--r--libraries/audio-test-harness/client-lib/Android.bp5
-rw-r--r--libraries/automotive-helpers/OWNERS6
-rw-r--r--libraries/car-helpers/multiuser-helper/Android.bp1
-rw-r--r--libraries/car-helpers/multiuser-helper/src/android/platform/helpers/MultiUserHelper.java21
-rw-r--r--libraries/collectors-helper/adservices/src/com/android/helpers/MeasurementLatencyHelper.java71
-rw-r--r--libraries/collectors-helper/adservices/test/src/com/android/helpers/MeasurementLatencyHelperTest.java105
-rw-r--r--libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java24
-rw-r--r--libraries/collectors-helper/perfetto/test/src/com/android/helpers/tests/PerfettoHelperTest.java9
-rw-r--r--libraries/compatibility-common-util/src/com/android/compatibility/common/util/LogcatInspector.java29
-rw-r--r--libraries/compatibility-common-util/src/com/android/compatibility/common/util/OWNERS6
-rw-r--r--libraries/compatibility-common-util/tests/src/com/android/compatibility/common/util/OWNERS2
-rw-r--r--libraries/device-collectors/src/main/java/android/device/collectors/MeasurementLatencyListener.java28
-rw-r--r--libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java17
-rw-r--r--libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.kt7
-rw-r--r--libraries/screenshot/Android.bp4
-rw-r--r--libraries/screenshot/src/main/java/platform/test/screenshot/DeviceEmulationRule.kt112
-rw-r--r--libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestListener.java65
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java184
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java93
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java11
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java69
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java106
-rw-r--r--libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java86
-rw-r--r--libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java90
-rw-r--r--libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java99
-rw-r--r--libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java32
-rw-r--r--libraries/sts-common-util/sts-sdk/package/README.md2
-rw-r--r--libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java123
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml8
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java98
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java13
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java38
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml23
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml21
-rw-r--r--libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java92
-rw-r--r--libraries/systemui-helper/src/android/platform/helpers/media/MediaController.java50
-rw-r--r--tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUser.java42
-rw-r--r--tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuest.java46
-rw-r--r--tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUser.java43
-rw-r--r--tests/automotive/health/multiuser/tests/Android.bp2
-rw-r--r--tests/automotive/health/multiuser/tests/AndroidTest.xml34
-rw-r--r--utils/shell-as/Android.bp107
-rw-r--r--utils/shell-as/AndroidManifest.xml.template33
-rw-r--r--utils/shell-as/OWNERS4
-rw-r--r--utils/shell-as/README.md33
-rw-r--r--utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java28
-rw-r--r--utils/shell-as/command-line.cpp210
-rw-r--r--utils/shell-as/command-line.h36
-rw-r--r--utils/shell-as/context.cpp138
-rw-r--r--utils/shell-as/context.h71
-rw-r--r--utils/shell-as/elf-utils.cpp89
-rw-r--r--utils/shell-as/elf-utils.h35
-rw-r--r--utils/shell-as/execute.cpp382
-rw-r--r--utils/shell-as/execute.h35
-rwxr-xr-xutils/shell-as/gen-manifest.sh43
-rw-r--r--utils/shell-as/registers.h44
-rw-r--r--utils/shell-as/shell-as-main.cpp93
-rw-r--r--utils/shell-as/shell-as-test-app-key.pk8bin0 -> 1218 bytes
-rw-r--r--utils/shell-as/shell-as-test-app-key.x509.pem24
-rw-r--r--utils/shell-as/shell-code.cpp82
-rw-r--r--utils/shell-as/shell-code.h40
-rw-r--r--utils/shell-as/shell-code/constants-arm.S24
-rw-r--r--utils/shell-as/shell-code/constants-arm64.S24
-rw-r--r--utils/shell-as/shell-code/constants-x86.S24
-rw-r--r--utils/shell-as/shell-code/constants-x86_64.S24
-rw-r--r--utils/shell-as/shell-code/constants.S30
-rw-r--r--utils/shell-as/shell-code/selinux-arm.S91
-rw-r--r--utils/shell-as/shell-code/selinux-arm64.S88
-rw-r--r--utils/shell-as/shell-code/selinux-x86.S91
-rw-r--r--utils/shell-as/shell-code/selinux-x86_64.S83
-rw-r--r--utils/shell-as/shell-code/trap-arm.S30
-rw-r--r--utils/shell-as/shell-code/trap-arm64.S28
-rw-r--r--utils/shell-as/shell-code/trap-x86.S28
-rw-r--r--utils/shell-as/shell-code/trap-x86_64.S28
-rw-r--r--utils/shell-as/string-utils.cpp72
-rw-r--r--utils/shell-as/string-utils.h50
-rw-r--r--utils/shell-as/test-app.cpp136
-rw-r--r--utils/shell-as/test-app.h31
87 files changed, 4394 insertions, 338 deletions
diff --git a/build/tasks/continuous_instrumentation_tests.mk b/build/tasks/continuous_instrumentation_tests.mk
index 1674d08cc..34050a4c1 100644
--- a/build/tasks/continuous_instrumentation_tests.mk
+++ b/build/tasks/continuous_instrumentation_tests.mk
@@ -52,7 +52,7 @@ api_xml := $(coverage_out)/api.xml
$(api_xml) : $(api_text) $(APICHECK)
$(hide) echo "Converting API file to XML: $@"
$(hide) mkdir -p $(dir $@)
- $(hide) $(APICHECK_COMMAND) -convert2xml $< $@
+ $(hide) $(APICHECK_COMMAND) signature-to-jdiff --strip $< $@
# CTS API coverage tool
api_coverage_exe := $(HOST_OUT_EXECUTABLES)/cts-api-coverage
diff --git a/build/tasks/tests/instrumentation_test_list.mk b/build/tasks/tests/instrumentation_test_list.mk
index 5b2fe8cc6..a9c256b80 100644
--- a/build/tasks/tests/instrumentation_test_list.mk
+++ b/build/tasks/tests/instrumentation_test_list.mk
@@ -70,7 +70,8 @@ instrumentation_tests := \
FrameworksPrivacyLibraryTests \
SettingsUITests \
SettingsPerfTests \
- ExtServicesUnitTests \
+ ExtServicesUnitTests-tplus \
+ ExtServicesUnitTests-sminus \
FrameworksNetSmokeTests \
FlickerLibTest \
FlickerTests \
diff --git a/build/tasks/tests/native_test_list.mk b/build/tasks/tests/native_test_list.mk
index b0bb7c232..e604371e9 100644
--- a/build/tasks/tests/native_test_list.mk
+++ b/build/tasks/tests/native_test_list.mk
@@ -94,7 +94,8 @@ native_tests := \
libnativehelper_tests \
libnetworkstats_test \
libprocinfo_test \
- libtextclassifier_tests \
+ libtextclassifier_tests-tplus \
+ libtextclassifier_tests-sminus \
libsurfaceflinger_unittest \
libunwindstack_unit_test \
libuwb_core_tests \
diff --git a/libraries/annotations/src/android/platform/test/annotations/IwTest.java b/libraries/annotations/src/android/platform/test/annotations/IwTest.java
deleted file mode 100644
index de44970d4..000000000
--- a/libraries/annotations/src/android/platform/test/annotations/IwTest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2022 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.platform.test.annotations;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Marks a test that should run as part of the Android team's effort in P0 CUJs platform testing
- * and automation enforcement.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface IwTest {
- /**
- * Defines the IW P0 CUJ focus area which the test class associates to.
- */
- String focusArea() default "";
-} \ No newline at end of file
diff --git a/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor2.java b/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor2.java
new file mode 100644
index 000000000..f9abfbb8f
--- /dev/null
+++ b/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor2.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 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.platform.helpers;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+/**
+ * @param <T> the helper interface under test.
+ * <p>A {@code HelperAccessor} can be included in any test to access an App Helper
+ * implementation.
+ * <p>For example: <code>
+ * {@code HelperAccessor<IXHelper> accessor = new HelperAccessor(IXHelper.class);}
+ * accessor.get().performSomeAction();
+ * </code> To target a specific helper implementation by prefix, build this object and call, <code>
+ * withPrefix</code> on it.
+ */
+public class HelperAccessor2<T extends IAppHelper2> {
+ private final Class<T> mInterfaceClass;
+
+ private T mHelper;
+ private String mPrefix;
+
+ public HelperAccessor2(Class<T> klass) {
+ mInterfaceClass = klass;
+ }
+
+ /** Selects only helpers that begin with the prefix, {@code prefix}. */
+ public HelperAccessor2<T> withPrefix(String prefix) {
+ mPrefix = prefix;
+ // Unset the helper, in case this was changed after first use.
+ mHelper = null;
+ // Return self to follow a pseudo-builder initialization pattern.
+ return this;
+ }
+
+ /** accessor.get().performSomeAction() {@code}. */
+ public T get() {
+ if (mHelper == null) {
+ if (mPrefix == null || mPrefix.isEmpty()) {
+ mHelper =
+ HelperManager.getInstance(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ InstrumentationRegistry.getInstrumentation())
+ .get(mInterfaceClass);
+ } else {
+ mHelper =
+ HelperManager.getInstance(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ InstrumentationRegistry.getInstrumentation())
+ .get(mInterfaceClass, mPrefix);
+ }
+ }
+ return mHelper;
+ }
+}
diff --git a/libraries/app-helpers/interfaces/Android.bp b/libraries/app-helpers/interfaces/Android.bp
index 05ca4aa95..9c6c1a46c 100644
--- a/libraries/app-helpers/interfaces/Android.bp
+++ b/libraries/app-helpers/interfaces/Android.bp
@@ -58,6 +58,7 @@ java_library {
libs: [
"ub-uiautomator",
"app-helpers-core",
+ "androidx.test.uiautomator_uiautomator",
],
static_libs: [
"app-helpers-common-interfaces",
diff --git a/libraries/app-helpers/interfaces/auto/OWNERS b/libraries/app-helpers/interfaces/auto/OWNERS
index 4beb6381b..fc0e765ad 100644
--- a/libraries/app-helpers/interfaces/auto/OWNERS
+++ b/libraries/app-helpers/interfaces/auto/OWNERS
@@ -1,2 +1,6 @@
schinchalkar@google.com
smara@google.com
+ewitt@google.com
+rrwoods@google.com
+shubhras@google.com
+vitalidim@google.com
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper2.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper2.java
new file mode 100644
index 000000000..cfafe4103
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper2.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2023 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.platform.helpers;
+
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiObject2;
+
+/** {@inheritDoc} */
+public interface IChromeHelper2 extends IAppHelper2 {
+ enum MenuItem {
+ BOOKMARKS("Bookmarks"),
+ NEW_TAB("New tab"),
+ CLOSE_ALL_TABS("Close all tabs"),
+ DOWNLOADS("Downloads"),
+ HISTORY("History"),
+ SETTINGS("Settings");
+
+ private final String mDisplayName;
+
+ MenuItem(String displayName) {
+ mDisplayName = displayName;
+ }
+
+ @Override
+ public String toString() {
+ return mDisplayName;
+ }
+ }
+
+ enum ClearRange {
+ PAST_HOUR("past hour"),
+ PAST_DAY("past day"),
+ PAST_WEEK("past week"),
+ LAST_4_WEEKS("last 4 weeks"),
+ BEGINNING_OF_TIME("beginning of time");
+
+ private final String mDisplayName;
+
+ ClearRange(String displayName) {
+ mDisplayName = displayName;
+ }
+
+ @Override
+ public String toString() {
+ return mDisplayName;
+ }
+ }
+
+ /**
+ * Setup expectations: Chrome is open and on a standard page, i.e. a tab is open.
+ *
+ * <p>This method will open the URL supplied and block until the page is open.
+ */
+ void openUrl(String url);
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will scroll the page as directed and block until idle.
+ */
+ void flingPage(Direction dir);
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will open the overload menu, indicated by three dots and block until open.
+ */
+ void openMenu();
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will open provided item in the menu.
+ */
+ void openMenuItem(IChromeHelper2.MenuItem menuItem);
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will open provided item in the menu.
+ *
+ * @param menuItem The name of menu item.
+ * @param waitForPageLoad Wait for the page to load completely or not.
+ */
+ default void openMenuItem(IChromeHelper2.MenuItem menuItem, boolean waitForPageLoad) {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will add a new tab and land on the webpage of given url.
+ */
+ void addNewTab(String url);
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will go to tab switcher by clicking tab switcher button.
+ */
+ void openTabSwitcher();
+
+ /**
+ * Setup expectations: Chrome is open on a page or in tab switcher.
+ *
+ * <p>This method will switch to the tab at tabIndex.
+ */
+ void switchTab(int tabIndex);
+
+ /**
+ * Setup expectations: Chrome has at least one tab.
+ *
+ * <p>This method will close all tabs.
+ */
+ void closeAllTabs();
+
+ /**
+ * Setup expectations: Chrome is open on a page and the tabs are treated as apps.
+ *
+ * <p>This method will change the settings to treat tabs inside of Chrome and block until Chrome
+ * is open on the original tab.
+ */
+ void mergeTabs();
+
+ /**
+ * Setup expectations: Chrome is open on a page and the tabs are merged.
+ *
+ * <p>This method will change the settings to treat tabs outside of Chrome and block until
+ * Chrome is open on the original tab.
+ */
+ void unmergeTabs();
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will reload the page by clicking the refresh button, and block until the page
+ * is reopened.
+ */
+ void reloadPage();
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will stop loading page then reload the page by clicking the refresh button,
+ * and block until the page is reopened.
+ */
+ default void stopAndReloadPage() {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will stop loading page then reload the page by clicking the refresh button,
+ *
+ * @param waitForPageLoad Wait for the page to load completely or not.
+ */
+ default void stopAndReloadPage(boolean waitForPageLoad) {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method is getter for contentDescription of Tab elements.
+ */
+ String getTabDescription();
+
+ /**
+ * Setup expectations: Chrome is open on a History page.
+ *
+ * <p>This method clears browser history for provided period of time.
+ */
+ void clearBrowsingData(IChromeHelper2.ClearRange range);
+
+ /**
+ * Setup expectations: Chrome is open on a Downloads page.
+ *
+ * <p>This method checks header is displayed on Downloads page.
+ */
+ void checkIfDownloadsOpened();
+
+ /**
+ * Setup expectations: Chrome is open on a Settings page.
+ *
+ * <p>This method clicks on Privacy setting on Settings page.
+ */
+ void openPrivacySettings();
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>This method will add the current page to Bookmarks
+ */
+ default boolean addCurrentPageToBookmark() {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectations: Chrome is open on a Bookmarks page.
+ *
+ * <p>This method selects a bookmark from the Bookmarks page.
+ *
+ * @param index The Index of bookmark to select.
+ */
+ default void openBookmark(int index) {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectations: Chrome is open on a Bookmarks page.
+ *
+ * <p>This method selects a bookmark from the Bookmarks page.
+ *
+ * @param bookmarkName The string of the target bookmark to select.
+ * @param waitForPageLoad Wait for the page to load completely or not.
+ */
+ default void openBookmark(String bookmarkName, boolean waitForPageLoad) {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>Selects the link with specific text.
+ *
+ * @param linkText The text of the link to select.
+ */
+ default void selectLink(String linkText) {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>Performs a scroll gesture on the page.
+ *
+ * @param dir The direction on the page to scroll.
+ * @param percent The distance to scroll as a percentage of the page's visible size.
+ */
+ default void scrollPage(Direction dir, float percent) {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * <p>Get the UiObject2 of the page screen.
+ */
+ default UiObject2 getWebPage() {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectation: Chrome was loading a web page.
+ *
+ * <p>Returns a boolean to state if current page is loaded.
+ */
+ default boolean isWebPageLoaded() {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+
+ /**
+ * Setup expectation: Chrome was loading a web page.
+ *
+ * <p>Checks number of active tabs.
+ */
+ default void tabsCount(int number) {
+ throw new UnsupportedOperationException("Not yet implemented.");
+ }
+}
diff --git a/libraries/app-helpers/spectatio/spectatio-util/src/android/platform/spectatio/configs/validators/ValidateUiElement.java b/libraries/app-helpers/spectatio/spectatio-util/src/android/platform/spectatio/configs/validators/ValidateUiElement.java
index 563e8d4e3..5fddd3fe7 100644
--- a/libraries/app-helpers/spectatio/spectatio-util/src/android/platform/spectatio/configs/validators/ValidateUiElement.java
+++ b/libraries/app-helpers/spectatio/spectatio-util/src/android/platform/spectatio/configs/validators/ValidateUiElement.java
@@ -30,6 +30,7 @@ import java.lang.reflect.Type;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* {@link ValidateUiElement} is a deserializer that validates Ui Elements in Spectatio JSON Config
@@ -88,7 +89,7 @@ public class ValidateUiElement implements JsonDeserializer<UiElement> {
List<UiElement> specifiers =
specifiersJson.asList().stream()
.map(element -> context.<UiElement>deserialize(element, typeOfT))
- .toList();
+ .collect(Collectors.toList());
int ancestorSpecifiers = 0;
for (UiElement specifier : specifiers) {
@@ -267,7 +268,7 @@ public class ValidateUiElement implements JsonDeserializer<UiElement> {
.map(Entry::getKey)
.map(String::trim)
.filter(key -> !mSupportedProperties.contains(key))
- .toList();
+ .collect(Collectors.toList());
if (!unknownProperties.isEmpty()) {
throw new RuntimeException(
String.format(
diff --git a/libraries/audio-test-harness/client-lib/Android.bp b/libraries/audio-test-harness/client-lib/Android.bp
index b057c69f3..8fa149474 100644
--- a/libraries/audio-test-harness/client-lib/Android.bp
+++ b/libraries/audio-test-harness/client-lib/Android.bp
@@ -63,10 +63,8 @@ java_library {
// TESTS ==============================================================
-java_test {
+java_test_host {
name: "audiotestharness-client-grpclib-tests",
- test_suites: ["general-tests"],
- host_supported: true,
srcs: [
"src/test/java/com/android/media/audiotestharness/client/grpc/*.java",
],
@@ -79,7 +77,6 @@ java_test {
"mockito",
"objenesis",
],
- sdk_version: "current",
test_options: {
unit_test: false,
},
diff --git a/libraries/automotive-helpers/OWNERS b/libraries/automotive-helpers/OWNERS
index 34ed1300d..146905a31 100644
--- a/libraries/automotive-helpers/OWNERS
+++ b/libraries/automotive-helpers/OWNERS
@@ -1,4 +1,6 @@
smara@google.com
schinchalkar@google.com
-aceyansf@google.com
-tongfei@google.com \ No newline at end of file
+ewitt@google.com
+rrwoods@google.com
+shubhras@google.com
+vitalidim@google.com
diff --git a/libraries/car-helpers/multiuser-helper/Android.bp b/libraries/car-helpers/multiuser-helper/Android.bp
index 9fd8040cf..32f4911b9 100644
--- a/libraries/car-helpers/multiuser-helper/Android.bp
+++ b/libraries/car-helpers/multiuser-helper/Android.bp
@@ -25,6 +25,7 @@ java_library_static {
static_libs: [
"android.car-test-stubs",
"androidx.test.runner",
+ "compatibility-device-util-axt",
"ub-uiautomator",
],
}
diff --git a/libraries/car-helpers/multiuser-helper/src/android/platform/helpers/MultiUserHelper.java b/libraries/car-helpers/multiuser-helper/src/android/platform/helpers/MultiUserHelper.java
index 16ccc71af..35b75f832 100644
--- a/libraries/car-helpers/multiuser-helper/src/android/platform/helpers/MultiUserHelper.java
+++ b/libraries/car-helpers/multiuser-helper/src/android/platform/helpers/MultiUserHelper.java
@@ -24,12 +24,15 @@ import android.car.user.UserSwitchResult;
import android.car.util.concurrent.AsyncFuture;
import android.content.Context;
import android.content.pm.UserInfo;
+import android.os.Build;
import android.os.SystemClock;
import android.os.UserManager;
import android.support.test.uiautomator.UiDevice;
import androidx.test.InstrumentationRegistry;
+import com.android.compatibility.common.util.SystemUtil;
+
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -42,6 +45,8 @@ public class MultiUserHelper {
/** For testing purpose we allow a wide range of switching time. */
private static final int USER_SWITCH_TIMEOUT_SECOND = 300;
+ private static final String SWITCH_USER_COMMAND = "cmd car_service switch-user ";
+
private static MultiUserHelper sMultiUserHelper;
private CarUserManager mCarUserManager;
private UserManager mUserManager;
@@ -102,6 +107,11 @@ public class MultiUserHelper {
* @param id Id of the user to switch to
*/
public void switchToUserId(int id) throws Exception {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ switchUserUsingShell(id);
+ return;
+ }
+
final CountDownLatch latch = new CountDownLatch(1);
// A UserLifeCycleListener to wait for user switch event. It is equivalent to
// UserSwitchObserver#onUserSwitchComplete callback
@@ -172,11 +182,16 @@ public class MultiUserHelper {
*/
@Nullable
public UserInfo getUserByName(String name) {
- return mUserManager
- .getAliveUsers()
- .stream()
+ return mUserManager.getUsers().stream()
.filter(user -> user.name.equals(name))
.findFirst()
.orElse(null);
}
+
+ private void switchUserUsingShell(int userId) throws Exception {
+ String retStr = SystemUtil.runShellCommand(SWITCH_USER_COMMAND + userId);
+ if (!retStr.contains("STATUS_SUCCESSFUL")) {
+ throw new Exception("failed to switch to user: " + userId);
+ }
+ }
}
diff --git a/libraries/collectors-helper/adservices/src/com/android/helpers/MeasurementLatencyHelper.java b/libraries/collectors-helper/adservices/src/com/android/helpers/MeasurementLatencyHelper.java
new file mode 100644
index 000000000..310511706
--- /dev/null
+++ b/libraries/collectors-helper/adservices/src/com/android/helpers/MeasurementLatencyHelper.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.helpers;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.time.Clock;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class MeasurementLatencyHelper {
+
+ public static LatencyHelper getLogcatCollector() {
+ return LatencyHelper.getLogcatLatencyHelper(new MeasurementProcessInputForLatencyMetrics());
+ }
+
+ @VisibleForTesting
+ public static LatencyHelper getCollector(
+ LatencyHelper.InputStreamFilter inputStreamFilter, Clock clock) {
+ return new LatencyHelper(
+ new MeasurementProcessInputForLatencyMetrics(), inputStreamFilter, clock);
+ }
+
+ private static class MeasurementProcessInputForLatencyMetrics
+ implements LatencyHelper.ProcessInputForLatencyMetrics {
+
+ @Override
+ public String getTestLabel() {
+ return "Measurement";
+ }
+
+ @Override
+ public Map<String, Long> processInput(InputStream inputStream) throws IOException {
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+ Pattern latencyMetricPattern =
+ Pattern.compile(getTestLabel() + ": \\((MEASUREMENT_LATENCY_.*): (\\d+) ms\\)");
+
+ String line = "";
+ Map<String, Long> output = new HashMap<String, Long>();
+ while ((line = bufferedReader.readLine()) != null) {
+ Matcher matcher = latencyMetricPattern.matcher(line);
+ while (matcher.find()) {
+ String metric = matcher.group(1);
+ long latency = Long.parseLong(matcher.group(2));
+ output.put(metric, latency);
+ }
+ }
+ return output;
+ }
+ }
+}
diff --git a/libraries/collectors-helper/adservices/test/src/com/android/helpers/MeasurementLatencyHelperTest.java b/libraries/collectors-helper/adservices/test/src/com/android/helpers/MeasurementLatencyHelperTest.java
new file mode 100644
index 000000000..9644413db
--- /dev/null
+++ b/libraries/collectors-helper/adservices/test/src/com/android/helpers/MeasurementLatencyHelperTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.helpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Map;
+
+public class MeasurementLatencyHelperTest {
+ private static final String TEST_FILTER_LABEL = "Measurement";
+ private static final String LOG_LABEL_REGISTER_SOURCE_HOT_START =
+ "MEASUREMENT_LATENCY_MeasurementLatencyTest#registerSource_hotStart";
+ private static final String LOG_LABEL_REGISTER_SOURCE_COLD_START =
+ "MEASUREMENT_LATENCY_MeasurementLatencyTest#registerSource_coldStart";
+
+ @Mock private LatencyHelper.InputStreamFilter mInputStreamFilter;
+
+ @Mock private Clock mClock;
+
+ private LatencyHelper mMeasurementLatencyHelper;
+
+ private Instant mInstant = Clock.systemUTC().instant();
+
+ @Before
+ public void setUo() {
+ MockitoAnnotations.initMocks(this);
+ when(mClock.getZone()).thenReturn(ZoneId.of("UTC"));
+ when(mClock.instant()).thenReturn(mInstant);
+ mMeasurementLatencyHelper =
+ MeasurementLatencyHelper.getCollector(mInputStreamFilter, mClock);
+ mMeasurementLatencyHelper.startCollecting();
+ }
+
+ @Test
+ public void testInitTimeInLogcat() throws IOException {
+ String logcatOutput =
+ "04-01 00:06:25.394 12970 I Measurement:"
+ + " (MEASUREMENT_LATENCY_MeasurementLatencyTest#registerSource_coldStart: 65"
+ + " ms)\n"
+ + "04-01 00:06:25.444 12970 I Measurement:"
+ + " (MEASUREMENT_LATENCY_MeasurementLatencyTest#registerSource_hotStart: 17"
+ + " ms)\n";
+
+ when(mInputStreamFilter.getStream(TEST_FILTER_LABEL, mInstant))
+ .thenReturn(new ByteArrayInputStream(logcatOutput.getBytes()));
+ Map<String, Long> actual = mMeasurementLatencyHelper.getMetrics();
+
+ assertThat(actual.get(LOG_LABEL_REGISTER_SOURCE_COLD_START)).isEqualTo(65);
+ assertThat(actual.get(LOG_LABEL_REGISTER_SOURCE_HOT_START)).isEqualTo(17);
+ }
+
+ @Test
+ public void testWithNonMatchingInput() throws IOException {
+ String logcatOutput = "Some random string";
+ when(mInputStreamFilter.getStream(TEST_FILTER_LABEL, mInstant))
+ .thenReturn(new ByteArrayInputStream(logcatOutput.getBytes()));
+ Map<String, Long> actual = mMeasurementLatencyHelper.getMetrics();
+
+ assertThat(actual.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void testWithEmptyLogcat() throws IOException {
+ when(mInputStreamFilter.getStream(TEST_FILTER_LABEL, mInstant))
+ .thenReturn(new ByteArrayInputStream("".getBytes()));
+ Map<String, Long> actual = mMeasurementLatencyHelper.getMetrics();
+ assertThat(actual.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void testInputStreamThrowsException() throws IOException {
+ when(mInputStreamFilter.getStream(TEST_FILTER_LABEL, mInstant))
+ .thenThrow(new IOException());
+ Map<String, Long> actual = mMeasurementLatencyHelper.getMetrics();
+ for (Long val : actual.values()) {
+ assertThat(val).isEqualTo(0);
+ }
+ }
+}
diff --git a/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java b/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java
index 707ec81da..de1dc9126 100644
--- a/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java
+++ b/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java
@@ -39,7 +39,9 @@ public class PerfettoHelper {
// returning from the original shell invocation.
// perfetto --background-wait -c /data/misc/perfetto-traces/trace_config.pb -o
// /data/misc/perfetto-traces/trace_output.perfetto-trace
- private static final String PERFETTO_START_CMD = "perfetto --background-wait -c %s%s -o %s";
+ private static final String PERFETTO_START_BG_WAIT_CMD =
+ "perfetto --background-wait -c %s%s -o %s";
+ private static final String PERFETTO_START_CMD = "perfetto --background -c %s%s -o %s";
private static final String PERFETTO_TMP_OUTPUT_FILE =
"/data/misc/perfetto-traces/trace_output.perfetto-trace";
// Additional arg to indicate that the perfetto config file is text format.
@@ -61,6 +63,8 @@ public class PerfettoHelper {
private String mConfigRootDir;
+ private boolean mPerfettoStartBgWait;
+
private int mPerfettoProcId = 0;
/**
@@ -91,8 +95,12 @@ public class PerfettoHelper {
PERFETTO_TMP_OUTPUT_FILE));
Log.i(LOG_TAG, String.format("Perfetto output file cleanup - %s", output));
- String perfettoCmd = String.format(PERFETTO_START_CMD,
- mConfigRootDir, configFileName, PERFETTO_TMP_OUTPUT_FILE);
+ String perfettoCmd =
+ String.format(
+ mPerfettoStartBgWait ? PERFETTO_START_BG_WAIT_CMD : PERFETTO_START_CMD,
+ mConfigRootDir,
+ configFileName,
+ PERFETTO_TMP_OUTPUT_FILE);
if(isTextProtoConfig) {
perfettoCmd = perfettoCmd + PERFETTO_TXT_PROTO_ARG;
@@ -106,6 +114,12 @@ public class PerfettoHelper {
mPerfettoProcId = Integer.parseInt(startOutput.trim());
}
+ // If the perfetto background wait option is not used then add a explicit wait after
+ // starting the perfetto trace.
+ if (!mPerfettoStartBgWait) {
+ SystemClock.sleep(1000);
+ }
+
if(!isTestPerfettoRunning()) {
return false;
}
@@ -240,4 +254,8 @@ public class PerfettoHelper {
public void setPerfettoConfigRootDir(String rootDir) {
mConfigRootDir = rootDir;
}
+
+ public void setPerfettoStartBgWait(boolean perfettoStartBgWait) {
+ mPerfettoStartBgWait = perfettoStartBgWait;
+ }
}
diff --git a/libraries/collectors-helper/perfetto/test/src/com/android/helpers/tests/PerfettoHelperTest.java b/libraries/collectors-helper/perfetto/test/src/com/android/helpers/tests/PerfettoHelperTest.java
index 72e06ad28..c6cd91718 100644
--- a/libraries/collectors-helper/perfetto/test/src/com/android/helpers/tests/PerfettoHelperTest.java
+++ b/libraries/collectors-helper/perfetto/test/src/com/android/helpers/tests/PerfettoHelperTest.java
@@ -53,6 +53,7 @@ public class PerfettoHelperTest {
public void setUp() {
mPerfettoHelper = new PerfettoHelper();
mPerfettoHelper.setPerfettoConfigRootDir("/data/misc/perfetto-traces/");
+ mPerfettoHelper.setPerfettoStartBgWait(true);
isPerfettoStartSuccess = false;
}
@@ -115,6 +116,14 @@ public class PerfettoHelperTest {
isPerfettoStartSuccess = true;
}
+ /** Test perfetto collection returns true if the background wait option is not used. */
+ @Test
+ public void testPerfettoStartSuccessNoBgWait() throws Exception {
+ mPerfettoHelper.setPerfettoStartBgWait(false);
+ assertTrue(mPerfettoHelper.startCollecting("trace_config.textproto", true));
+ isPerfettoStartSuccess = true;
+ }
+
/**
* Test if the path name is prefixed with /.
*/
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/LogcatInspector.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/LogcatInspector.java
index bd7b00cd2..997f62e17 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/LogcatInspector.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/LogcatInspector.java
@@ -96,17 +96,34 @@ public abstract class LogcatInspector {
throws InterruptedException, IOException {
long timeout = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(timeInSeconds);
int stringIndex = 0;
+ long lastEpochMicroseconds = 0;
while (timeout >= System.currentTimeMillis()) {
- stringIndex = 0;
- InputStream logcatStream = executeShellCommand("logcat -v brief -d " + filterSpec);
+ // '-v epoch' -> Displays time as seconds since Jan 1 1970.
+ // '-v usec' -> Displays time down the microsecond precision.
+ InputStream logcatStream =
+ executeShellCommand("logcat -v epoch -v usec -d " + filterSpec);
BufferedReader logcat = new BufferedReader(new InputStreamReader(logcatStream));
String line;
while ((line = logcat.readLine()) != null) {
if (line.contains(logcatStrings[stringIndex])) {
- stringIndex++;
- if (stringIndex >= logcatStrings.length) {
- StreamUtil.drainAndClose(logcat);
- return stringIndex;
+ // Now we need to get the timestamp of this log line to ensure that
+ // this log is after the previously matched log.
+
+ // Strip the leading spaces and split the line by spaces
+ String[] splitLine = line.stripLeading().split(" ");
+
+ // The first one is epoch time in seconds, with microsecond precision.
+ // It is of the format <epoch time in seconds>.xxxxxx
+ String epochMicrosecondsStr = splitLine[0].replace(".", "");
+ long epochMicroseconds = Long.parseLong(epochMicrosecondsStr);
+
+ // Check that this log time is after previously matched log
+ if (epochMicroseconds >= lastEpochMicroseconds) {
+ stringIndex++;
+ if (stringIndex >= logcatStrings.length) {
+ StreamUtil.drainAndClose(logcat);
+ return stringIndex;
+ }
}
}
}
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/OWNERS b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/OWNERS
index be00f2c7e..e81b71f27 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/OWNERS
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/OWNERS
@@ -1,2 +1,6 @@
per-file ReasonType.java = set noparent
-per-file ReasonType.java = ape-relpgm-cls@google.com \ No newline at end of file
+per-file ReasonType.java = ape-relpgm-cls@google.com
+per-file BackupUtils.java = set noparent
+per-file BackupUtils.java = file:platform/frameworks/base:/services/backup/OWNERS
+per-file LogcatInspector.java = set noparent
+per-file LogcatInspector.java = file:platform/frameworks/base:/services/backup/OWNERS \ No newline at end of file
diff --git a/libraries/compatibility-common-util/tests/src/com/android/compatibility/common/util/OWNERS b/libraries/compatibility-common-util/tests/src/com/android/compatibility/common/util/OWNERS
new file mode 100644
index 000000000..34a50227d
--- /dev/null
+++ b/libraries/compatibility-common-util/tests/src/com/android/compatibility/common/util/OWNERS
@@ -0,0 +1,2 @@
+per-file BackupUtilsTest.java = set noparent
+per-file BackupUtilsTest.java = file:platform/frameworks/base:/services/backup/OWNERS \ No newline at end of file
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/MeasurementLatencyListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/MeasurementLatencyListener.java
new file mode 100644
index 000000000..2d9ef03bc
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/MeasurementLatencyListener.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+
+import com.android.helpers.MeasurementLatencyHelper;
+
+@OptionClass(alias = "measurement-latency-listener")
+public class MeasurementLatencyListener extends BaseCollectionListener<Long> {
+ public MeasurementLatencyListener() {
+ createHelperInstance(MeasurementLatencyHelper.getLogcatCollector());
+ }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java
index 2626bc9d0..9085e3d19 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java
@@ -22,9 +22,15 @@ import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.util.Log;
+
import androidx.annotation.VisibleForTesting;
+
import com.android.helpers.PerfettoHelper;
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -32,9 +38,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;
-import org.junit.runner.Description;
-import org.junit.runner.Result;
-import org.junit.runner.notification.Failure;
/**
* A {@link PerfettoListener} that captures the perfetto trace during each test method
@@ -70,6 +73,7 @@ public class PerfettoListener extends BaseMetricListener {
private static final String PERFETTO_FILE_PATH = "perfetto_file_path";
// Collect per run if it is set to true otherwise collect per test.
public static final String COLLECT_PER_RUN = "per_run";
+ public static final String PERFETTO_START_BG_WAIT = "perfetto_start_bg_wait";
public static final String PERFETTO_PREFIX = "perfetto_";
// Skip failure metrics collection if this flag is set to true.
public static final String SKIP_TEST_FAILURE_METRICS = "skip_test_failure_metrics";
@@ -104,6 +108,8 @@ public class PerfettoListener extends BaseMetricListener {
private boolean mPerfettoStartSuccess = false;
private boolean mIsConfigTextProto = false;
private boolean mIsCollectPerRun;
+ // Enable the perfetto background wait during perfetto trace startup by default.
+ private boolean mPerfettoStartBgWait = true;
private boolean mSkipTestFailureMetrics;
private boolean mIsTestFailed = false;
@@ -291,6 +297,11 @@ public class PerfettoListener extends BaseMetricListener {
// Whether to collect the for the entire test run or per test.
mIsCollectPerRun = Boolean.parseBoolean(args.getString(COLLECT_PER_RUN));
+ // Option used to decide whether to use background wait option during perfetto start.
+ mPerfettoStartBgWait =
+ Boolean.parseBoolean(args.getString(PERFETTO_START_BG_WAIT, String.valueOf(true)));
+ mPerfettoHelper.setPerfettoStartBgWait(mPerfettoStartBgWait);
+
// Root directory path containing the perfetto config file.
mConfigRootDir =
args.getString(PERFETTO_CONFIG_ROOT_DIR_ARG, DEFAULT_PERFETTO_CONFIG_ROOT_DIR);
diff --git a/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.kt b/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.kt
index c758aa085..f8e5ecd53 100644
--- a/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.kt
+++ b/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.kt
@@ -77,8 +77,11 @@ class ScreenRecordRule : TestRule {
private fun shouldRecordScreen(description: Description): Boolean {
return if (description.isTest) {
- description.getAnnotation(ScreenRecord::class.java) != null ||
- testLevelOverrideEnabled()
+ val screenRecordBinaryAvailable = File("/system/bin/screenrecord").exists()
+ log("screenRecordBinaryAvailable: $screenRecordBinaryAvailable")
+ screenRecordBinaryAvailable &&
+ (description.getAnnotation(ScreenRecord::class.java) != null ||
+ testLevelOverrideEnabled())
} else { // class level
description.testClass.hasAnnotation(ScreenRecord::class.java) ||
classLevelOverrideEnabled()
diff --git a/libraries/screenshot/Android.bp b/libraries/screenshot/Android.bp
index 71adc6292..f22389eec 100644
--- a/libraries/screenshot/Android.bp
+++ b/libraries/screenshot/Android.bp
@@ -25,7 +25,8 @@ android_test {
enabled: false
},
srcs: [
- "src/**/*.kt"
+ "src/**/*.java",
+ "src/**/*.kt",
],
static_libs: [
"androidx.test.core",
@@ -52,6 +53,7 @@ android_library {
enabled: false
},
srcs: [
+ "src/main/java/platform/test/screenshot/**/*.java",
"src/main/java/platform/test/screenshot/**/*.kt",
],
static_libs: [
diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/DeviceEmulationRule.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/DeviceEmulationRule.kt
index b49f1aae7..fa732e2af 100644
--- a/libraries/screenshot/src/main/java/platform/test/screenshot/DeviceEmulationRule.kt
+++ b/libraries/screenshot/src/main/java/platform/test/screenshot/DeviceEmulationRule.kt
@@ -46,17 +46,20 @@ class DeviceEmulationRule(private val spec: DeviceEmulationSpec) : TestRule {
private val uiAutomation = instrumentation.uiAutomation
private val isRoblectric = Build.FINGERPRINT.contains("robolectric")
+ companion object {
+ var prevDensity: Int? = -1
+ var prevWidth: Int? = -1
+ var prevHeight: Int? = -1
+ var prevNightMode: Int? = UiModeManager.MODE_NIGHT_AUTO
+ var initialized: Boolean = false
+ }
+
override fun apply(base: Statement, description: Description): Statement {
- // The statement which calls beforeTest() before running the test and afterTest()
- // afterwards.
+ // The statement which calls beforeTest() before running the test.
return object : Statement() {
override fun evaluate() {
- try {
- beforeTest()
- base.evaluate()
- } finally {
- afterTest()
- }
+ beforeTest()
+ base.evaluate()
}
}
}
@@ -76,32 +79,41 @@ class DeviceEmulationRule(private val spec: DeviceEmulationSpec) : TestRule {
val qualifier = "w${width}dp-h${height}dp-${density}dpi"
setQualifiers.invoke(null, qualifier)
} else {
- // Make sure that we are in natural orientation (rotation 0) before we set the screen size
- uiAutomation.setRotation(UiAutomation.ROTATION_FREEZE_0)
-
- val wm = WindowManagerGlobal.getWindowManagerService()
- wm.setForcedDisplayDensityForUser(
- Display.DEFAULT_DISPLAY,
- density,
- UserHandle.myUserId()
- )
- wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, width, height)
-
- // Force the dark/light theme.
- val uiModeManager =
- InstrumentationRegistry.getInstrumentation()
- .targetContext
- .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
- uiModeManager.setApplicationNightMode(
+ val curNightMode =
if (spec.isDarkTheme) {
UiModeManager.MODE_NIGHT_YES
} else {
UiModeManager.MODE_NIGHT_NO
}
- )
- // Make sure that all devices are in touch mode to avoid screenshot differences
- // in focused elements when in keyboard mode
- instrumentation.setInTouchMode(true)
+
+ if (initialized) {
+ if (prevDensity != density) {
+ setDisplayDensity(density)
+ }
+ if (prevWidth != width || prevHeight != height) {
+ setDisplaySize(width, height)
+ }
+ if (prevNightMode != curNightMode) {
+ setNightMode(curNightMode)
+ }
+ } else {
+ // Make sure that we are in natural orientation (rotation 0) before we set the
+ // screen size.
+ uiAutomation.setRotation(UiAutomation.ROTATION_FREEZE_0)
+
+ setDisplayDensity(density)
+ setDisplaySize(width, height)
+
+ // Force the dark/light theme.
+ setNightMode(curNightMode)
+
+ // Make sure that all devices are in touch mode to avoid screenshot differences
+ // in focused elements when in keyboard mode.
+ instrumentation.setInTouchMode(true)
+
+ // Set the initialization fact.
+ initialized = true
+ }
}
}
@@ -116,27 +128,33 @@ class DeviceEmulationRule(private val spec: DeviceEmulationSpec) : TestRule {
}
}
- private fun afterTest() {
- // Reset the density and display size.
- if (isRoblectric) {
- return
- }
-
+ private fun setDisplayDensity(density: Int) {
val wm = WindowManagerGlobal.getWindowManagerService()
- wm.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, UserHandle.myUserId())
- wm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY)
-
- // Reset the dark/light theme.
- val uiModeManager =
- InstrumentationRegistry.getInstrumentation()
- .targetContext
- .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
- uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO)
+ wm.setForcedDisplayDensityForUser(
+ Display.DEFAULT_DISPLAY,
+ density,
+ UserHandle.myUserId()
+ )
+ prevDensity = density
+ }
- instrumentation.resetInTouchMode()
+ private fun setDisplaySize(
+ width: Int,
+ height: Int
+ ) {
+ val wm = WindowManagerGlobal.getWindowManagerService()
+ wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, width, height)
+ prevWidth = width
+ prevHeight = height
+ }
- // Unfreeze locked rotation
- uiAutomation.setRotation(UiAutomation.ROTATION_UNFREEZE)
+ private fun setNightMode(nightMode: Int) {
+ val uiModeManager =
+ InstrumentationRegistry.getInstrumentation()
+ .targetContext
+ .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
+ uiModeManager.setApplicationNightMode(nightMode)
+ prevNightMode = nightMode
}
}
diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestListener.java b/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestListener.java
new file mode 100644
index 000000000..1c19f22fb
--- /dev/null
+++ b/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestListener.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 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 platform.test.screenshot;
+
+import android.app.UiAutomation;
+import android.app.UiModeManager;
+import android.content.Context;
+import android.os.Build;
+import android.os.UserHandle;
+import android.view.Display;
+import android.view.IWindowManager;
+import android.view.WindowManagerGlobal;
+
+import androidx.test.internal.runner.listener.InstrumentationRunListener;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.runner.Result;
+
+/** A test listener for cleaning up after all screenshot tests are done. */
+public class ScreenshotTestListener extends InstrumentationRunListener {
+
+ private static final String TAG = "ScreenshotTestListener";
+
+ @Override
+ public void testRunFinished(Result result) throws Exception {
+ // Skip cleaning up if we run Robolectric tests.
+ if (Build.FINGERPRINT.contains("robolectric")) {
+ return;
+ }
+
+ // Reset the density and display size.
+ IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+ wm.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, UserHandle.myUserId());
+ wm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY);
+
+ // Reset the dark/light theme.
+ UiModeManager uiModeManager =
+ (UiModeManager)
+ (InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getSystemService(Context.UI_MODE_SERVICE));
+ uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO);
+
+ InstrumentationRegistry.getInstrumentation().resetInTouchMode();
+
+ // Unfreeze locked rotation
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .setRotation(UiAutomation.ROTATION_UNFREEZE);
+ }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java
new file mode 100644
index 000000000..e4997d913
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/** Util to parse dumpsys */
+public class DumpsysUtils {
+
+ /**
+ * Fetch dumpsys for the service
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param args the arguments {@link String} to filter output
+ * @return the raw output without newline character
+ * @throws Exception
+ */
+ public static String getRawDumpsys(ITestDevice device, String args) throws Exception {
+ CommandResult output = device.executeShellV2Command("dumpsys " + args);
+ if (output.getStatus() != CommandStatus.SUCCESS) {
+ throw new IllegalStateException(
+ String.format(
+ "Failed to get dumpsys for %s details, due to : %s",
+ args, output.toString()));
+ }
+ return output.getStdout();
+ }
+
+ /**
+ * Parse the dumpsys for the service using pattern
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param service the service name {@link String} to check status.
+ * @param args the argument {@link Map} to filter output
+ * @param pattern the pattern {@link String} to parse the dumpsys output
+ * @param matcherFlag the flags {@link String} to look while parsing the dumpsys output
+ * @return the required value.
+ * @throws Exception
+ */
+ public static Matcher getParsedDumpsys(
+ ITestDevice device,
+ String service,
+ Map<String, String> args,
+ String pattern,
+ int matcherFlag)
+ throws Exception {
+ String arguments =
+ args == null
+ ? ""
+ : args.entrySet().stream()
+ .map(arg -> String.format("%s %s", arg.getKey(), arg.getValue()))
+ .collect(Collectors.joining(" "));
+ String rawOutput = getRawDumpsys(device, String.format("%s %s", service, arguments));
+ rawOutput =
+ String.join(
+ "",
+ Arrays.stream(rawOutput.split("\n"))
+ .map(e -> e.trim())
+ .toArray(String[]::new));
+ return Pattern.compile(pattern, matcherFlag).matcher(rawOutput);
+ }
+
+ /**
+ * Parse the dumpsys for the service using pattern
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param service the service name {@link String} to check status.
+ * @param pattern the pattern {@link String} to parse the dumpsys output
+ * @param matcherFlag the flags {@link String} to look while parsing the dumpsys output
+ * @return the required value.
+ * @throws Exception
+ */
+ public static Matcher getParsedDumpsys(
+ ITestDevice device, String service, String pattern, int matcherFlag) throws Exception {
+ return getParsedDumpsys(device, service, null /* args */, pattern, matcherFlag);
+ }
+
+ /**
+ * Check if output contains mResumed=true for the activity
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param activityName the activity name {@link String} to check status.
+ * @return true, if mResumed=true. Else false.
+ * @throws Exception
+ */
+ public static boolean hasActivityResumed(ITestDevice device, String activityName)
+ throws Exception {
+ return getParsedDumpsys(
+ device,
+ "activity" /* service */,
+ Map.of("-a", activityName) /* args */,
+ "mResumed=true" /* pattern */,
+ Pattern.CASE_INSENSITIVE /* matcherFlag */)
+ .find();
+ }
+
+ /**
+ * Check if output contains mVisible=true for the activity
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param activityName the activity name {@link String} to check status.
+ * @return true, if mVisible=true. Else false.
+ * @throws Exception
+ */
+ public static boolean isActivityVisible(ITestDevice device, String activityName)
+ throws Exception {
+ return getParsedDumpsys(
+ device,
+ "activity" /* service */,
+ Map.of("-a", activityName) /* args */,
+ "mVisible=true" /* pattern */,
+ Pattern.CASE_INSENSITIVE /* matcherFlag */)
+ .find();
+ }
+
+ /**
+ * Fetch the role-holder-name for the role-name under the userid
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param roleName the role name {@link String} to fetch role holder's name.
+ * @param userId the userid {@link int} to fetch role holder's name for the user.
+ * @return holder name, if exits. Else null.
+ * @throws Exception
+ */
+ public static String getRoleHolder(ITestDevice device, String roleName, int userId)
+ throws Exception {
+ // Fetch roles for the user
+ Matcher rolesMatcher =
+ getParsedDumpsys(
+ device,
+ "role" /* service */,
+ String.format("user_id=%d.+?roles=(?<roles>\\[.+?])", userId) /* pattern */,
+ Pattern.CASE_INSENSITIVE);
+ if (!rolesMatcher.find()) {
+ return null;
+ }
+
+ // Fetch the holder's name for the role
+ Matcher holderMatcher =
+ Pattern.compile(
+ String.format("\\{name=%sholders=(?<holders>.+?)}", roleName),
+ Pattern.CASE_INSENSITIVE)
+ .matcher(rolesMatcher.group("roles"));
+ if (!holderMatcher.find()) {
+ return null;
+ }
+ return holderMatcher.group("holders").trim();
+ }
+
+ /**
+ * Fetch the role-holder-name for the role-name
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param roleName the role name {@link String} to fetch role holder's name for the current
+ * user.
+ * @return holder name, if exits. Else null.
+ * @throws Exception
+ */
+ public static String getRoleHolder(ITestDevice device, String roleName) throws Exception {
+ return getRoleHolder(device, roleName, device.getCurrentUser());
+ }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java
index 12665b2fb..214ccc0d6 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java
@@ -17,9 +17,6 @@
package com.android.sts.common;
import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeNoException;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
@@ -48,71 +45,89 @@ public class MallocDebug implements AutoCloseable {
Pattern.compile("^.*HAS INVALID TAG.*$", Pattern.MULTILINE),
};
- private ITestDevice device;
- private String processName;
- private AutoCloseable setMallocDebugOptionsProperty;
- private AutoCloseable setAttachedProgramProperty;
- private AutoCloseable killProcess;
+ private ITestDevice mDevice;
+ private String mProcessName;
+ private boolean mWasAdbRoot;
+ private AutoCloseable mSetMallocDebugOptionsProperty;
+ private AutoCloseable mSetAttachedProgramProperty;
+ private AutoCloseable mProcessKill;
private MallocDebug(
ITestDevice device, String mallocDebugOption, String processName, boolean isService)
throws DeviceNotAvailableException, TimeoutException, ProcessUtil.KillException {
- this.device = device;
- this.processName = processName;
-
- // It's an error if this is called while something else is also doing malloc debug.
- assertNull(
- MALLOC_DEBUG_OPTIONS_PROP + " is already set!",
- device.getProperty(MALLOC_DEBUG_OPTIONS_PROP));
- CommandUtil.runAndCheck(device, "logcat -c");
+ mDevice = device;
+ mProcessName = processName;
+ mWasAdbRoot = device.isAdbRoot();
+
+ String previousProperty = device.getProperty(MALLOC_DEBUG_OPTIONS_PROP);
+ if (previousProperty != null) {
+ // log if this is called while something else is also doing malloc debug.
+ CLog.w("%s is already set! <%s>", MALLOC_DEBUG_OPTIONS_PROP, previousProperty);
+ }
try {
- this.setMallocDebugOptionsProperty =
+ mSetMallocDebugOptionsProperty =
SystemUtil.withProperty(device, MALLOC_DEBUG_OPTIONS_PROP, mallocDebugOption);
- this.setAttachedProgramProperty =
+ mSetAttachedProgramProperty =
SystemUtil.withProperty(device, MALLOC_DEBUG_PROGRAM_PROP, processName);
+ CommandUtil.runAndCheck(device, "logcat -c");
+
// Kill and wait for the process to come back if we're attaching to a service
- this.killProcess = null;
+ mProcessKill = null;
if (isService) {
- this.killProcess = ProcessUtil.withProcessKill(device, processName, null);
+ mProcessKill = ProcessUtil.withProcessKill(device, processName, null);
ProcessUtil.waitProcessRunning(device, processName);
}
} catch (Throwable e1) {
try {
- if (setMallocDebugOptionsProperty != null) {
- setMallocDebugOptionsProperty.close();
+ if (mSetMallocDebugOptionsProperty != null) {
+ mSetMallocDebugOptionsProperty.close();
}
- if (setAttachedProgramProperty != null) {
- setAttachedProgramProperty.close();
+ if (mSetAttachedProgramProperty != null) {
+ mSetAttachedProgramProperty.close();
}
} catch (Exception e2) {
CLog.e(e2);
- fail(
+ throw new IllegalStateException(
"Could not enable malloc debug. Additionally, there was an"
+ " exception while trying to reset device state. Tests after"
- + " this may not work as expected!\n"
- + e2);
+ + " this may not work as expected!",
+ e2);
}
- assumeNoException("Could not enable malloc debug", e1);
+ throw new IllegalStateException("Could not enable malloc debug", e1);
}
}
@Override
public void close() throws Exception {
- device.waitForDeviceAvailable();
- setMallocDebugOptionsProperty.close();
- setAttachedProgramProperty.close();
- if (killProcess != null) {
+ mDevice.waitForDeviceAvailable();
+ boolean isAdbRoot = mDevice.isAdbRoot();
+ if (mWasAdbRoot) {
+ // regain root permissions to teardown
+ mDevice.enableAdbRoot();
+ }
+ try {
+ mSetMallocDebugOptionsProperty.close();
+ mSetAttachedProgramProperty.close();
+ } catch (Exception e) {
+ throw new IllegalStateException("Could not disable malloc debug", e);
+ }
+ if (mProcessKill != null) {
try {
- killProcess.close();
- ProcessUtil.waitProcessRunning(device, processName);
+ mProcessKill.close();
+ ProcessUtil.waitProcessRunning(mDevice, mProcessName);
} catch (TimeoutException e) {
- assumeNoException(
- "Could not restart '" + processName + "' after disabling malloc debug", e);
+ throw new IllegalStateException(
+ "Could not restart '" + mProcessName + "' after disabling malloc debug", e);
}
}
- String logcat = CommandUtil.runAndCheck(device, "logcat -d *:S malloc_debug:V").getStdout();
+ String logcat =
+ CommandUtil.runAndCheck(mDevice, "logcat -d *:S malloc_debug:V").getStdout();
+ if (!isAdbRoot) {
+ // restore nonroot status if the try-with-resources body unrooted
+ mDevice.disableAdbRoot();
+ }
assertNoMallocDebugErrors(logcat);
}
@@ -129,7 +144,7 @@ public class MallocDebug implements AutoCloseable {
public static AutoCloseable withLibcMallocDebugOnService(
ITestDevice device, String mallocDebugOptions, String processName)
throws DeviceNotAvailableException, IllegalArgumentException, TimeoutException,
- ProcessUtil.KillException {
+ ProcessUtil.KillException {
if (processName == null || processName.isEmpty()) {
throw new IllegalArgumentException("Service processName can't be empty");
}
@@ -149,7 +164,7 @@ public class MallocDebug implements AutoCloseable {
public static AutoCloseable withLibcMallocDebugOnNewProcess(
ITestDevice device, String mallocDebugOptions, String processName)
throws DeviceNotAvailableException, IllegalArgumentException, TimeoutException,
- ProcessUtil.KillException {
+ ProcessUtil.KillException {
if (processName == null || processName.isEmpty()) {
throw new IllegalArgumentException("processName can't be empty");
}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
index bea14ff15..d8fd05fda 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
@@ -429,18 +429,21 @@ public final class ProcessUtil {
*
* @param device device to be run on
* @param process pgrep pattern of process to look for
- * @param filenameSubstr part of file name/path loaded by the process
+ * @param filenamePattern the filename pattern to find
* @return an Opotional of IFileEntry of the path of the file on the device if exists.
*/
public static Optional<IFileEntry> findFileLoadedByProcess(
- ITestDevice device, String process, String filenameSubstr)
+ ITestDevice device, String process, Pattern filenamePattern)
throws DeviceNotAvailableException {
Optional<Integer> pid = ProcessUtil.pidOf(device, process);
if (pid.isPresent()) {
- String cmd = "lsof -p " + pid.get().toString() + " | awk '{print $NF}'";
+ String cmd = "lsof -p " + pid.get().toString() + " | grep -o '/.*'";
String[] openFiles = CommandUtil.runAndCheck(device, cmd).getStdout().split("\n");
for (String f : openFiles) {
- if (f.contains(filenameSubstr)) {
+ if (f.contains("Permission denied")) {
+ throw new IllegalStateException("no permission to read open files for process");
+ }
+ if (filenamePattern.matcher(f).find()) {
return Optional.of(device.getFileEntry(f.trim()));
}
}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java
index 9465c1c00..7f8d8bc86 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java
@@ -20,6 +20,9 @@ import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
+import java.util.HashMap;
+import java.util.Map;
+
/** Util to manage secondary user */
public class UserUtils {
@@ -34,27 +37,32 @@ public class UserUtils {
private boolean mIsPreCreateOnly; // User type : --pre-created-only
private boolean mIsRestricted; // User type : --restricted
private boolean mSwitch; // Switch to newly created user
- private int mDisallowAppInstall; // Disallow app installation in secondary user explicitly
private int mProfileOf; // Userid associated with managed user
private int mTestUserId;
+ private Map<String, String> mUserRestrictions; // Map of user-restrictions for new user
/**
* Create an instance of secondary user.
*
* @param device the device {@link ITestDevice} to use.
- * @throws IllegalArgumentException when {@code device} is null.
+ * @throws Exception
*/
- public SecondaryUser(ITestDevice device) throws IllegalArgumentException {
+ public SecondaryUser(ITestDevice device) throws Exception {
// Device should not be null
if (device == null) {
throw new IllegalArgumentException("Device should not be null");
}
+ // Check if device supports multiple users
+ if (!device.isMultiUserSupported()) {
+ throw new IllegalStateException("Device does not support multiple users");
+ }
+
mDevice = device;
mName = "testUser"; /* Default username */
+ mUserRestrictions = new HashMap<String, String>();
// Set default value for all flags as false
- mDisallowAppInstall = 0; // 0 - allow app installation, 1 - disallow
mIsDemo = false;
mIsEphemeral = false;
mIsForTesting = false;
@@ -66,16 +74,6 @@ public class UserUtils {
}
/**
- * Disallow app installation in secondary user explicitly
- *
- * @return this object for method chaining.
- */
- public SecondaryUser disallowAppInstallation() {
- mDisallowAppInstall = 1; // 0 - allow app installation, 1 - disallow
- return this;
- }
-
- /**
* Set the user type as demo.
*
* @return this object for method chaining.
@@ -175,6 +173,17 @@ public class UserUtils {
}
/**
+ * Set user-restrictions on newly created secondary user.
+ * Note: Setting user-restrictions requires enabling root.
+ *
+ * @return this object for method chaining.
+ */
+ public SecondaryUser withUserRestrictions(Map<String, String> restrictions) {
+ mUserRestrictions.putAll(restrictions);
+ return this;
+ }
+
+ /**
* Create a secondary user and if required, switch to it. Returns an Autocloseable that
* removes the secondary user.
*
@@ -231,17 +240,27 @@ public class UserUtils {
String.format("Failed to start the user: %s", mTestUserId));
}
- // Enable/Disable app installation in secondary user
- final CommandResult userRestrictionCmdOutput =
- mDevice.executeShellV2Command(
- String.format(
- "pm set-user-restriction --user %d no_install_apps %d",
- mTestUserId, mDisallowAppInstall));
- if (userRestrictionCmdOutput.getStatus() != CommandStatus.SUCCESS) {
- throw new IllegalStateException(
- String.format(
- "Failed to set user restriction 'no_install_apps' with message: %s",
- userRestrictionCmdOutput.toString()));
+ // Add user-restrictions to newly created secondary user
+ if (!mUserRestrictions.isEmpty()) {
+ if (!mDevice.isAdbRoot()) {
+ throw new IllegalStateException("Setting user-restriction requires root");
+ }
+
+ for (Map.Entry<String, String> entry : mUserRestrictions.entrySet()) {
+ final CommandResult cmdOutput =
+ mDevice.executeShellV2Command(
+ String.format(
+ "pm set-user-restriction --user %d %s %s",
+ mTestUserId, entry.getKey(), entry.getValue()));
+ if (cmdOutput.getStatus() != CommandStatus.SUCCESS) {
+ asSecondaryUser.close();
+ throw new IllegalStateException(
+ String.format(
+ "Failed to set user restriction %s value %s with"
+ + " message %s",
+ entry.getKey(), entry.getValue(), cmdOutput.toString()));
+ }
+ }
}
// Switch to the user if required and the user type is neither managed nor
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java
new file mode 100644
index 000000000..25252d12a
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.util;
+
+import com.android.sts.common.CommandUtil;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+
+/** Tools for comparing kernel versions */
+public final class KernelVersionHost {
+
+ private KernelVersionHost() {}
+
+ /**
+ * Get the device kernel version
+ *
+ * @param device The device to collect the kernel version from
+ */
+ public static KernelVersion getKernelVersion(ITestDevice device)
+ throws DeviceNotAvailableException {
+ // https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md
+ // uname is part of Android since 6.0 Marshmallow
+ CommandResult res = CommandUtil.runAndCheck(device, "uname -r");
+ return KernelVersion.parse(res.getStdout());
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionEqualTo(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) == 0;
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionLessThan(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) < 0;
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionLessThanEqualTo(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) <= 0;
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionGreaterThan(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) > 0;
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionGreaterThanEqualTo(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) >= 0;
+ }
+}
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java
index 03b19341d..f82a90756 100644
--- a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java
@@ -16,9 +16,16 @@
package com.android.sts.common;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import org.junit.After;
+import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -45,6 +52,26 @@ public class MallocDebugTest extends BaseHostJUnit4Test {
}
}
+ @Before
+ public void setUp() throws Exception {
+ assertWithMessage("libc.debug.malloc.options not empty before test")
+ .that(getDevice().getProperty("libc.debug.malloc.options"))
+ .isNull();
+ assertWithMessage("libc.debug.malloc.programs not empty before test")
+ .that(getDevice().getProperty("libc.debug.malloc.programs"))
+ .isNull();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ assertWithMessage("libc.debug.malloc.options not empty after test")
+ .that(getDevice().getProperty("libc.debug.malloc.options"))
+ .isNull();
+ assertWithMessage("libc.debug.malloc.programs not empty after test")
+ .that(getDevice().getProperty("libc.debug.malloc.programs"))
+ .isNull();
+ }
+
@Test(expected = Test.None.class /* no exception expected */)
public void testMallocDebugNoErrors() throws Exception {
MallocDebug.assertNoMallocDebugErrors(logcatWithoutErrors);
@@ -54,4 +81,63 @@ public class MallocDebugTest extends BaseHostJUnit4Test {
public void testMallocDebugWithErrors() throws Exception {
MallocDebug.assertNoMallocDebugErrors(logcatWithErrors);
}
+
+ @Test(expected = IllegalStateException.class)
+ public void testMallocDebugAutocloseableNonRoot() throws Exception {
+ assertTrue(getDevice().disableAdbRoot());
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(
+ getDevice(), "backtrace guard", "native-poc")) {
+ // empty
+ }
+ }
+
+ @Test
+ public void testMallocDebugAutocloseableRoot() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(
+ getDevice(), "backtrace guard", "native-poc")) {
+ // empty
+ }
+ }
+
+ @Test
+ public void testMallocDebugAutocloseableNonRootCleanup() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(
+ getDevice(), "backtrace guard", "native-poc")) {
+ assertTrue("could not disable root", getDevice().disableAdbRoot());
+ }
+ assertFalse(
+ "device should not be root after autoclose if the body unrooted",
+ getDevice().isAdbRoot());
+ }
+
+ @Test
+ public void testMallocDebugAutoseablePriorValueNoException() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ final String oldValue = "TEST_VALUE_OLD";
+ final String newValue = "TEST_VALUE_NEW";
+ assertTrue(
+ "could not set libc.debug.malloc.options",
+ getDevice().setProperty("libc.debug.malloc.options", oldValue));
+ assertWithMessage("test property was not properly set on device")
+ .that(getDevice().getProperty("libc.debug.malloc.options"))
+ .isEqualTo(oldValue);
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(getDevice(), newValue, "native-poc")) {
+ assertWithMessage("new property was not set during malloc debug body")
+ .that(getDevice().getProperty("libc.debug.malloc.options"))
+ .isEqualTo(newValue);
+ }
+ String afterValue = getDevice().getProperty("libc.debug.malloc.options");
+ assertTrue(
+ "could not clear libc.debug.malloc.options",
+ getDevice().setProperty("libc.debug.malloc.options", ""));
+ assertWithMessage("prior property was not restored after test")
+ .that(afterValue)
+ .isEqualTo(oldValue);
+ }
}
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java
new file mode 100644
index 000000000..c6f4c6963
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.device.IFileEntry;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+/** Unit tests for {@link ProcessUtil}. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class ProcessUtilTest extends BaseHostJUnit4Test {
+
+ @Before
+ public void setUp() throws Exception {
+ assertTrue("could not unroot", getDevice().disableAdbRoot());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ assertTrue("could not unroot", getDevice().disableAdbRoot());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testFindLoadedByProcessNonRoot() throws Exception {
+ // expect failure because the shell user has no permission to read process info of other
+ // users
+ ProcessUtil.findFileLoadedByProcess(
+ getDevice(), "system_server", Pattern.compile(Pattern.quote("libc.so")));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testFindLoadedByProcessMultipleProcesses() throws Exception {
+ // pattern 'android' has multiple (android.hardware.drm, android.hardware.gnss, etc)
+ ProcessUtil.findFileLoadedByProcess(getDevice(), "android", null);
+ }
+
+ @Test
+ public void testFindLoadedByProcessUtilRoot() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ Optional<IFileEntry> fileEntryOptional =
+ ProcessUtil.findFileLoadedByProcess(
+ getDevice(), "system_server", Pattern.compile(Pattern.quote("libc.so")));
+ assertWithMessage("file entry should not be empty")
+ .that(fileEntryOptional.isPresent())
+ .isTrue();
+ IFileEntry fileEntry = fileEntryOptional.get();
+ assertWithMessage("file entry should be a path to libc.so")
+ .that(fileEntry.getFullPath())
+ .contains("libc.so");
+ }
+
+ @Test
+ public void testFindLoadedByProcessUtilNoMatch() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ Optional<IFileEntry> fileEntryOptional =
+ ProcessUtil.findFileLoadedByProcess(
+ getDevice(),
+ "system_server",
+ Pattern.compile(Pattern.quote("doesnotexist.foobar")));
+ assertWithMessage("file entry should be empty if no matches")
+ .that(fileEntryOptional.isPresent())
+ .isFalse();
+ }
+}
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java
new file mode 100644
index 000000000..aa58f1022
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/** Unit tests for {@link UserUtils}. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class UserUtilsTest extends BaseHostJUnit4Test {
+ private static final String TEST_USER_NAME = "TestUserForUserUtils";
+ private static final String CMD_PM_LIST_USERS = "pm list users";
+
+ @Before
+ public void setUp() throws Exception {
+ assertWithMessage("device already has the test user")
+ .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout())
+ .doesNotContain(TEST_USER_NAME);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ assertWithMessage("did not clean up the test user")
+ .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout())
+ .doesNotContain(TEST_USER_NAME);
+ }
+
+ @Test
+ public void testUserUtilsNonRoot() throws Exception {
+ assertTrue(getDevice().disableAdbRoot());
+ try (AutoCloseable user =
+ new UserUtils.SecondaryUser(getDevice()).name(TEST_USER_NAME).withUser()) {
+ assertFalse(
+ "device should not implicitly root to create a user", getDevice().isAdbRoot());
+ assertWithMessage("did not create the test user")
+ .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout())
+ .contains(TEST_USER_NAME);
+ }
+ assertFalse("device should not implicitly root to cleanup", getDevice().isAdbRoot());
+ }
+
+ @Test
+ public void testUserUtilsRoot() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ try (AutoCloseable user =
+ new UserUtils.SecondaryUser(getDevice()).name(TEST_USER_NAME).withUser()) {
+ assertTrue(
+ "device should still be root after user creation if started with root",
+ getDevice().isAdbRoot());
+ assertWithMessage("did not create the test user")
+ .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout())
+ .contains(TEST_USER_NAME);
+ }
+ assertTrue(
+ "device should still be root after cleanup if started with root",
+ getDevice().isAdbRoot());
+ }
+
+ @Test
+ public void testUserUtilsUserRestriction() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ try (AutoCloseable user =
+ new UserUtils.SecondaryUser(getDevice())
+ .name(TEST_USER_NAME)
+ .withUserRestrictions(Map.of("test_restriction", "1"))
+ .withUser()) {
+ // Exception is thrown if any error occurs while setting user restriction above
+ }
+ assertTrue(
+ "device should still be root after cleanup if started with root",
+ getDevice().isAdbRoot());
+ }
+}
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java
new file mode 100644
index 000000000..3a4603978
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.util;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class KernelVersionHostTest extends BaseHostJUnit4Test {
+
+ @Test
+ public final void testGetKernelVersion() throws Exception {
+ KernelVersionHost.getKernelVersion(getDevice());
+ }
+}
diff --git a/libraries/sts-common-util/sts-sdk/package/README.md b/libraries/sts-common-util/sts-sdk/package/README.md
index 663994aa3..41b399a14 100644
--- a/libraries/sts-common-util/sts-sdk/package/README.md
+++ b/libraries/sts-common-util/sts-sdk/package/README.md
@@ -1,2 +1,2 @@
-See https://source.android.com/docs/security/test/sts-sdK for instructions and
+See https://source.android.com/docs/security/test/sts-sdk for instructions and
documentation.
diff --git a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java
index 2e27305ef..fe824cfd4 100644
--- a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java
+++ b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java
@@ -16,17 +16,30 @@
package android.security.sts;
-import static com.android.sts.common.CommandUtil.runAndCheck;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import com.android.sts.common.CommandUtil;
+import com.android.sts.common.MallocDebug;
import com.android.sts.common.NativePoc;
+import com.android.sts.common.NativePocCrashAsserter;
import com.android.sts.common.NativePocStatusAsserter;
+import com.android.sts.common.ProcessUtil;
+import com.android.sts.common.RegexUtils;
+import com.android.sts.common.SystemUtil;
+import com.android.sts.common.UserUtils;
import com.android.sts.common.tradefed.testtype.NonRootSecurityTestCase;
+import com.android.sts.common.util.TombstoneUtils;
+import com.android.tradefed.device.IFileEntry;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
@RunWith(DeviceJUnit4ClassRunner.class)
public class StsHostSideTestCase extends NonRootSecurityTestCase {
@@ -34,32 +47,126 @@ public class StsHostSideTestCase extends NonRootSecurityTestCase {
static final String TEST_PKG = "android.security.sts.sts_test_app_package";
static final String TEST_CLASS = TEST_PKG + "." + "DeviceTest";
+ /** An app test, which uses this host Java test to launch an Android instrumented test */
@Test
public void testWithApp() throws Exception {
- // Note: this test is for CVE-2020-0215
ITestDevice device = getDevice();
- device.enableAdbRoot();
+ assertTrue("could not disable root", device.disableAdbRoot());
uninstallPackage(device, TEST_PKG);
- runAndCheck(device, "input keyevent KEYCODE_WAKEUP");
- runAndCheck(device, "input keyevent KEYCODE_MENU");
- runAndCheck(device, "input keyevent KEYCODE_HOME");
-
installPackage(TEST_APP);
runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod");
}
+ /**
+ * A native PoC test, which uses this host Java test to push an executable with resources and
+ * execute with environment variables and more. This API uses a "NativePocAsserter" that handles
+ * the most common ways to retrieve data from the native PoC. It can be overloaded to handle the
+ * specific side-effect that your PoC generates. It also demonstrates how to add extra memory
+ * checking with Malloc Debug.
+ */
@Test
public void testWithNativePoc() throws Exception {
NativePoc.builder()
+ // the name of the PoC
.pocName("native-poc")
+ // extra files pushed to the device
.resources("res.txt")
+ // command-line arguments for the PoC
.args("res.txt", "arg2")
+ // other options allow different linker paths for library shims
.useDefaultLdLibraryPath(true)
+ // test ends with ASSUMPTION_FAILURE if not EXIT_OK
.assumePocExitSuccess(true)
+ // run code after the PoC is executed for cleanup or other
.after(r -> getDevice().executeShellV2Command("ls -l /"))
- .asserter(NativePocStatusAsserter.assertNotVulnerableExitCode()) // not 113
+ // fail if the poc returns exit status 113
+ .asserter(NativePocStatusAsserter.assertNotVulnerableExitCode())
+ .build()
+ .run(this);
+ }
+
+ /** Run native PoCs with Malloc Debug memory checking enabled */
+ @Test
+ public void testWithMallocDebug() throws Exception {
+ // Set up Malloc Debug for this test, which may be required if the vulnerability needs
+ // memory checking to crash. This is useful when an ASan/HWASan/MTE build is not available.
+ // https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(
+ getDevice(),
+ "backtrace guard", // malloc debug options
+ "native-poc" // process name
+ )) {
+ // run a native PoC
+ NativePoc.builder()
+ .pocName("native-poc")
+ .build() // add more as needed
+ .run(this);
+ }
+ }
+
+ /** Run code after applying device settings */
+ @Test
+ public void testWithSetting() throws Exception {
+ // allow reflection, which is not a security boundary
+ try (AutoCloseable setting =
+ SystemUtil.withSetting(getDevice(), "global", "hidden_api_policy", "1")) {
+ // run app
+ installPackage(TEST_APP);
+ runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod");
+ }
+ }
+
+ /** Link a native PoC against a vulnerable system library */
+ @Test
+ public void testWithVulnerableLibrary() throws Exception {
+ // get the path of the vulnerable library
+ Optional<IFileEntry> libFileEntry =
+ ProcessUtil.findFileLoadedByProcess(
+ getDevice(), "media.metrics", Pattern.quote("libmediametrics.so"));
+ assumeTrue("shared library not loaded by target process", libFileEntry.isPresent());
+
+ // attack the service
+ NativePoc.builder()
+ .pocName("native-poc")
+ // pass the library path to the PoC
+ .args(libFileEntry.get().getFullPath())
+ .asserter(
+ NativePocCrashAsserter.assertNoCrash(
+ new TombstoneUtils.Config()
+ // Because the vulnerability is in the shared library, the
+ // process crash is the PoC.
+ .setProcessPatterns(Pattern.compile("native-poc"))))
.build()
.run(this);
}
+
+ /** Match a log against a known vulnerable pattern regex */
+ @Test
+ public void testWithLogMessage() throws Exception {
+ // this is only for dmesg/logcat messages that are not controlled by the test.
+
+ // attack the device, which can be native poc, echo to socket, send intent, app, etc
+ NativePoc.builder()
+ .pocName("native-poc")
+ .build() // add more as needed
+ .run(this);
+
+ String dmesg = CommandUtil.runAndCheck(getDevice(), "dmesg -c").getStdout();
+
+ // It's preferred to use this for matching text because the regex has a timeout to
+ // protect against catastrophic backtracking. It also formats the test assert message.
+ RegexUtils.assertNotContainsMultiline(
+ "Call trace:.*?__arm_lpae_unmap.*?kgsl_iommu_unmap", dmesg);
+ }
+
+ /** Install and run an app as a secondary user */
+ @Test
+ public void testWithSecondaryUser() throws Exception {
+ try (AutoCloseable su = new UserUtils.SecondaryUser(getDevice()).restricted().withUser()) {
+ installPackage(TEST_APP);
+ runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod");
+ }
+ }
}
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml
index b7f8ac87e..a16eccb9d 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml
@@ -32,13 +32,5 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- <receiver android:name=".PocReceiver"
- android:exported="true">
- <intent-filter>
- <action android:name="com.android.nfc.handover.action.ALLOW_CONNECT" />
- <action android:name="com.android.nfc.handover.action.DENY_CONNECT" />
- <action android:name="com.android.nfc.handover.action.TIMEOUT_CONNECT" />
- </intent-filter>
- </receiver>
</application>
</manifest>
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java
index da1f7bf47..a218e81f6 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java
@@ -18,56 +18,82 @@ package android.security.sts.sts_test_app_package;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assume.assumeNoException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.SharedPreferences;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
-import androidx.annotation.IntegerRes;
-import androidx.annotation.StringRes;
-import androidx.test.runner.AndroidJUnit4;
-import androidx.test.uiautomator.UiDevice;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * An example device test that starts an activity and uses broadcasts to wait for the artifact
+ * proving vulnerability
+ */
@RunWith(AndroidJUnit4.class)
public class DeviceTest {
+ private static final String TAG = DeviceTest.class.getSimpleName();
+ Context mContext;
- Context mAppContext;
+ /** Test broadcast action */
+ public static final String ACTION_BROADCAST = "action_security_test_broadcast";
+ /** Broadcast intent extra name for artifacts */
+ public static final String INTENT_ARTIFACT = "artifact";
- int getInteger(@IntegerRes int resId) {
- return mAppContext.getResources().getInteger(resId);
- }
+ /** Device test */
+ @Test
+ public void testDeviceSideMethod() throws Exception {
+ mContext = getApplicationContext();
- String getString(@StringRes int resId) {
- return mAppContext.getResources().getString(resId);
- }
+ AtomicReference<String> actual = new AtomicReference<>();
+ final Semaphore broadcastReceived = new Semaphore(0);
+ BroadcastReceiver broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ if (!intent.getAction().equals(ACTION_BROADCAST)) {
+ Log.i(
+ TAG,
+ String.format(
+ "got a broadcast that we didn't expect: %s",
+ intent.getAction()));
+ }
+ actual.set(intent.getStringExtra(INTENT_ARTIFACT));
+ broadcastReceived.release();
+ } catch (Exception e) {
+ Log.e(TAG, "got an exception when handling broadcast", e);
+ }
+ }
+ };
+ IntentFilter filter = new IntentFilter(); // see if there's a shorthand
+ filter.addAction(ACTION_BROADCAST); // what does this return?
+ mContext.registerReceiver(broadcastReceiver, filter);
- @Test
- public void testDeviceSideMethod() {
+ // start the target app
try {
- mAppContext = getApplicationContext();
- UiDevice device = UiDevice.getInstance(getInstrumentation());
- device.executeShellCommand(
- "am start -n com.android.nfc/.handover.ConfirmConnectActivity");
- long startTime = System.currentTimeMillis();
- while ((System.currentTimeMillis() - startTime)
- < getInteger(R.integer.MAX_WAIT_TIME_MS)) {
- SharedPreferences sh =
- mAppContext.getSharedPreferences(
- getString(R.string.SHARED_PREFERENCE), Context.MODE_APPEND);
- int result =
- sh.getInt(getString(R.string.RESULT_KEY), getInteger(R.integer.DEFAULT));
- assertNotEquals(
- "NFC Android App broadcasts Bluetooth device information!",
- result,
- getInteger(R.integer.FAIL));
- Thread.sleep(getInteger(R.integer.SLEEP_TIME_MS));
- }
- } catch (Exception e) {
- assumeNoException(e);
+ Log.d(TAG, "starting local activity");
+ Intent newActivityIntent = new Intent(mContext, PocActivity.class);
+ newActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // this could be startActivityForResult, but is generic for illustrative purposes
+ mContext.startActivity(newActivityIntent);
+ } finally {
+ getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
+ assertTrue(
+ "Timed out when getting result from other activity",
+ broadcastReceived.tryAcquire(/* TIMEOUT_MS */ 5000, TimeUnit.MILLISECONDS));
+ assertEquals("The target artifact should have been 'secure'", "secure", actual.get());
}
}
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java
index 27d682d19..daeb76c8b 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java
@@ -17,13 +17,26 @@
package android.security.sts.sts_test_app_package;
import android.app.Activity;
+import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
public class PocActivity extends Activity {
+ private static final String TAG = PocActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
+ Log.d(TAG, "poc activity started");
+
+ // Collect the artifact representing vulnerability here.
+ // Change this to whatever type best fits the vulnerable artifact; consider using a bundle
+ // if there are multiple artifacts necessary to prove the security vulnerability.
+ String artifact = "vulnerable";
+
+ Intent vulnerabilityDescriptionIntent = new Intent(DeviceTest.ACTION_BROADCAST);
+ vulnerabilityDescriptionIntent.putExtra(DeviceTest.INTENT_ARTIFACT, artifact);
+ this.sendBroadcast(vulnerabilityDescriptionIntent);
}
}
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java
deleted file mode 100644
index ac879258e..000000000
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2022 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.security.sts.sts_test_app_package;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-
-public class PocReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- SharedPreferences sh =
- context.getSharedPreferences(
- context.getResources().getString(R.string.SHARED_PREFERENCE),
- Context.MODE_PRIVATE);
- SharedPreferences.Editor edit = sh.edit();
- edit.putInt(
- context.getResources().getString(R.string.RESULT_KEY),
- context.getResources().getInteger(R.integer.FAIL));
- edit.commit();
- }
-}
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml
deleted file mode 100644
index acdcd84b6..000000000
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2022 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.
- -->
-
-<resources>
- <integer name="DEFAULT">0</integer>
- <integer name="FAIL">1</integer>
- <integer name="SLEEP_TIME_MS">500</integer>
- <integer name="MAX_WAIT_TIME_MS">10000</integer>
-</resources>
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml
deleted file mode 100644
index 286e6fd69..000000000
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2022 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.
- -->
-
-<resources>
- <string name="RESULT_KEY">result</string>
- <string name="SHARED_PREFERENCE">sts_test_app_failure</string>
-</resources>
diff --git a/libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java b/libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java
new file mode 100644
index 000000000..c9329b969
--- /dev/null
+++ b/libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Tools for parsing kernel version strings */
+public final class KernelVersion implements Comparable<KernelVersion> {
+ public final int version;
+ public final int patchLevel;
+ public final int subLevel;
+
+ public KernelVersion(int version, int patchLevel, int subLevel) {
+ this.version = version;
+ this.patchLevel = patchLevel;
+ this.subLevel = subLevel;
+ }
+
+ /**
+ * Parse a kernel version string in the format "version.patchlevel.sublevel" - "5.4.123".
+ * Trailing values are ignored so `uname -r` can be parsed properly.
+ *
+ * @param versionString The version string to parse
+ */
+ public static KernelVersion parse(String versionString) {
+ Pattern kernelReleasePattern =
+ Pattern.compile("(?<version>\\d+)\\.(?<patchLevel>\\d+)\\.(?<subLevel>\\d+)(.*)");
+ Matcher matcher = kernelReleasePattern.matcher(versionString);
+ if (matcher.find()) {
+ return new KernelVersion(
+ Integer.parseInt(matcher.group("version")),
+ Integer.parseInt(matcher.group("patchLevel")),
+ Integer.parseInt(matcher.group("subLevel")));
+ }
+ throw new IllegalArgumentException(
+ String.format("Could not parse kernel version string (%s)", versionString));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ // 2147483647 (INT_MAX)
+ // vvppppssss
+ return version * 10000000 + patchLevel * 10000 + subLevel;
+ }
+
+ /** Compare by version, patchlevel, and sublevel in that order. */
+ public int compareTo(KernelVersion o) {
+ if (version != o.version) {
+ return Integer.compare(version, o.version);
+ }
+ if (patchLevel != o.patchLevel) {
+ return Integer.compare(patchLevel, o.patchLevel);
+ }
+ return Integer.compare(subLevel, o.subLevel);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof KernelVersion) {
+ return this.compareTo((KernelVersion) o) == 0;
+ }
+ return false;
+ }
+
+ /** Format as "version.patchlevel.sublevel" */
+ @Override
+ public String toString() {
+ return String.format("%d.%d.%d", version, patchLevel, subLevel);
+ }
+
+ /** Format as "version.patchlevel" */
+ public String toStringShort() {
+ return String.format("%d.%d", version, patchLevel);
+ }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/media/MediaController.java b/libraries/systemui-helper/src/android/platform/helpers/media/MediaController.java
index be2f19ed8..69a6b1d31 100644
--- a/libraries/systemui-helper/src/android/platform/helpers/media/MediaController.java
+++ b/libraries/systemui-helper/src/android/platform/helpers/media/MediaController.java
@@ -21,6 +21,7 @@ import android.graphics.Rect;
import android.media.MediaMetadata;
import android.media.session.PlaybackState;
import android.platform.test.scenario.tapl_common.Gestures;
+import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.By;
@@ -36,7 +37,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class MediaController {
-
+ private static final String TAG = MediaController.class.getSimpleName();
private static final String PKG = "com.android.systemui";
private static final String HIDE_BTN_RES = "dismiss";
private static final BySelector PLAY_BTN_SELECTOR =
@@ -56,6 +57,7 @@ public class MediaController {
private final UiDevice mDevice = UiDevice.getInstance(mInstrumentation);
private final List<Integer> mStateChanges;
private Runnable mStateListener;
+ private static final Object sStateListenerLock = new Object();
MediaController(MediaInstrumentation media, UiObject2 uiObject) {
media.addMediaSessionStateChangedListeners(this::onMediaSessionStageChanged);
@@ -65,17 +67,21 @@ public class MediaController {
public void play() {
runToNextState(
- () -> mUiObject
- .wait(Until.findObject(PLAY_BTN_SELECTOR), WAIT_TIME_MILLIS)
- .click(),
- PlaybackState.STATE_PLAYING);
+ () -> {
+ mInstrumentation.getUiAutomation().clearCache();
+ mUiObject.wait(Until.findObject(PLAY_BTN_SELECTOR), WAIT_TIME_MILLIS).click();
+ },
+ PlaybackState.STATE_PLAYING);
}
public void pause() {
runToNextState(
- () -> Gestures.click(
- mUiObject.wait(Until.findObject(PAUSE_BTN_SELECTOR), WAIT_TIME_MILLIS),
- "Pause button"),
+ () -> {
+ mInstrumentation.getUiAutomation().clearCache();
+ Gestures.click(
+ mUiObject.wait(Until.findObject(PAUSE_BTN_SELECTOR), WAIT_TIME_MILLIS),
+ "Pause button");
+ },
PlaybackState.STATE_PAUSED);
}
@@ -98,11 +104,14 @@ public class MediaController {
private void runToNextState(Runnable runnable, int state) {
mStateChanges.clear();
CountDownLatch latch = new CountDownLatch(1);
- mStateListener = latch::countDown;
+ synchronized (sStateListenerLock) {
+ mStateListener = latch::countDown;
+ }
runnable.run();
try {
if (!latch.await(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS)) {
- throw new RuntimeException("PlaybackState didn't change and timeout.");
+ throw new RuntimeException(
+ "PlaybackState didn't change to state:" + state + " and timeout.");
}
} catch (InterruptedException e) {
throw new RuntimeException();
@@ -115,8 +124,10 @@ public class MediaController {
private void onMediaSessionStageChanged(int state) {
mStateChanges.add(state);
if (mStateListener != null) {
- mStateListener.run();
- mStateListener = null;
+ synchronized (sStateListenerLock) {
+ mStateListener.run();
+ mStateListener = null;
+ }
}
}
@@ -153,12 +164,25 @@ public class MediaController {
* @return boolean
*/
public boolean hasMetadata(MediaMetadata meta) {
+ Log.d(
+ TAG,
+ "[Check metadata] hasMetadata: By.header_title="
+ + meta.getString(MediaMetadata.METADATA_KEY_TITLE)
+ + "By.header_artist="
+ + meta.getString(MediaMetadata.METADATA_KEY_ARTIST));
final BySelector mediaTitleSelector =
By.res(PKG, "header_title").text(meta.getString(MediaMetadata.METADATA_KEY_TITLE));
final BySelector mediaArtistSelector =
By.res(PKG, "header_artist")
.text(meta.getString(MediaMetadata.METADATA_KEY_ARTIST));
- return mUiObject.hasObject(mediaTitleSelector) && mUiObject.hasObject(mediaArtistSelector);
+ mInstrumentation.getUiAutomation().clearCache();
+ final boolean titleCheckResult = mUiObject.hasObject(mediaTitleSelector);
+ final boolean artistCheckResult = mUiObject.hasObject(mediaArtistSelector);
+
+ Log.d(
+ TAG,
+ "[Check metadata] title: " + titleCheckResult + ". artist: " + artistCheckResult);
+ return titleCheckResult && artistCheckResult;
}
public boolean swipe(Direction direction) {
diff --git a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUser.java b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUser.java
index 4de4442cf..4b6cd79e1 100644
--- a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUser.java
+++ b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUser.java
@@ -16,10 +16,14 @@
package android.platform.scenario.multiuser;
+import android.app.UiAutomation;
import android.content.pm.UserInfo;
import android.os.SystemClock;
import android.platform.helpers.MultiUserHelper;
import android.platform.test.scenario.annotation.Scenario;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -37,33 +41,65 @@ public class SwitchToExistingSecondaryUser {
private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
private int mTargetUserId;
+ private UiAutomation mUiAutomation = null;
+ private static final String CREATE_USERS_PERMISSION = "android.permission.CREATE_USERS";
@Before
public void setup() throws Exception {
- /*
- TODO: Create setup util API
- */
+ /*
+ TODO: Create setup util API
+ */
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation = getUiAutomation();
+ // TODO: b/302175460 - update minimum SDK version
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
+
UserInfo targetUser = mMultiUserHelper
.getUserByName(MultiUserConstants.SECONDARY_USER_NAME);
+
if (targetUser == null) {
// Create new user and switch to it for the first time
mTargetUserId = mMultiUserHelper
.createUser(MultiUserConstants.SECONDARY_USER_NAME, false);
+
// In order to skip reporting the duration for the first time a user is created,
// always switch to newly created user for the first time it is created during setup.
mMultiUserHelper.switchAndWaitForStable(
mTargetUserId, MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
}
+
UserInfo currentUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
+
if (currentUser.id != MultiUserConstants.DEFAULT_INITIAL_USER) {
SystemClock.sleep(MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation = getUiAutomation();
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
+
mMultiUserHelper.switchAndWaitForStable(
MultiUserConstants.DEFAULT_INITIAL_USER, MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
}
}
@Test
public void testSwitch() throws Exception {
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation = getUiAutomation();
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
mMultiUserHelper.switchToUserId(mTargetUserId);
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+
+ private UiAutomation getUiAutomation() {
+ return InstrumentationRegistry.getInstrumentation().getUiAutomation();
}
}
diff --git a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuest.java b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuest.java
index 4a9c24790..facc1ac6f 100644
--- a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuest.java
+++ b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuest.java
@@ -16,10 +16,14 @@
package android.platform.scenario.multiuser;
+import android.app.UiAutomation;
import android.content.pm.UserInfo;
import android.os.SystemClock;
import android.platform.helpers.MultiUserHelper;
import android.platform.test.scenario.annotation.Scenario;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -37,28 +41,64 @@ public class SwitchToNewGuest {
private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
private int mGuestId;
+ private UiAutomation mUiAutomation = null;
+ private static final String CREATE_USERS_PERMISSION = "android.permission.CREATE_USERS";
@Before
public void setup() throws Exception {
- /*
- TODO: Create setup util API
- */
+ /*
+ TODO: Create setup util API
+ */
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation = getUiAutomation();
+ // TODO: b/302175460 - update minimum SDK version
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
UserInfo currentUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
+
if (currentUser.id != MultiUserConstants.DEFAULT_INITIAL_USER) {
SystemClock.sleep(MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation = getUiAutomation();
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
mMultiUserHelper.switchAndWaitForStable(
MultiUserConstants.DEFAULT_INITIAL_USER, MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
}
+
if (!MultiUserConstants.INCLUDE_CREATION_TIME) {
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation = getUiAutomation();
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
mGuestId = mMultiUserHelper.createUser(MultiUserConstants.GUEST_NAME, true);
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
}
}
@Test
public void testSwitch() throws Exception {
+
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation = getUiAutomation();
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
+
if (MultiUserConstants.INCLUDE_CREATION_TIME) {
mGuestId = mMultiUserHelper.createUser(MultiUserConstants.GUEST_NAME, true);
}
mMultiUserHelper.switchToUserId(mGuestId);
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+
+ private UiAutomation getUiAutomation() {
+ return InstrumentationRegistry.getInstrumentation().getUiAutomation();
}
}
diff --git a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUser.java b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUser.java
index 8bfb20d18..7696f6ce6 100644
--- a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUser.java
+++ b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUser.java
@@ -16,10 +16,14 @@
package android.platform.scenario.multiuser;
+import android.app.UiAutomation;
import android.content.pm.UserInfo;
import android.os.SystemClock;
import android.platform.helpers.MultiUserHelper;
import android.platform.test.scenario.annotation.Scenario;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -37,20 +41,40 @@ public class SwitchToNewSecondaryUser {
private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
private int mTargetUserId;
+ private UiAutomation mUiAutomation = null;
+ private static final String CREATE_USERS_PERMISSION = "android.permission.CREATE_USERS";
@Before
public void setup() throws Exception {
- /*
- TODO(b/194536236): Refactor setup code in multiuser nonui tests and create setup util API instead
- */
+ /*
+ TODO(b/194536236): Refactor setup code in multiuser nonui tests
+ * and create setup util API instead
+ */
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation = getUiAutomation();
+ // TODO: b/302175460 - update minimum SDK version
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
UserInfo currentUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
+
if (currentUser.id != MultiUserConstants.DEFAULT_INITIAL_USER) {
SystemClock.sleep(MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
mMultiUserHelper.switchAndWaitForStable(
MultiUserConstants.DEFAULT_INITIAL_USER, MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
}
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
UserInfo targetUser = mMultiUserHelper
.getUserByName(MultiUserConstants.SECONDARY_USER_NAME);
+
if (targetUser != null) {
if (!mMultiUserHelper.removeUser(targetUser)) {
throw new Exception("Failed to remove user: " + targetUser.id);
@@ -60,14 +84,27 @@ public class SwitchToNewSecondaryUser {
mTargetUserId = mMultiUserHelper
.createUser(MultiUserConstants.SECONDARY_USER_NAME, false);
}
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
}
@Test
public void testSwitch() throws Exception {
+ // Execute user manager APIs with elevated permissions
+ mUiAutomation = getUiAutomation();
+ mUiAutomation.adoptShellPermissionIdentity(CREATE_USERS_PERMISSION);
if (MultiUserConstants.INCLUDE_CREATION_TIME) {
mTargetUserId = mMultiUserHelper
.createUser(MultiUserConstants.SECONDARY_USER_NAME, false);
}
mMultiUserHelper.switchToUserId(mTargetUserId);
+
+ // Drop elevated permissions
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+
+ private UiAutomation getUiAutomation() {
+ return InstrumentationRegistry.getInstrumentation().getUiAutomation();
}
}
diff --git a/tests/automotive/health/multiuser/tests/Android.bp b/tests/automotive/health/multiuser/tests/Android.bp
index 8409848fc..3be54b4e8 100644
--- a/tests/automotive/health/multiuser/tests/Android.bp
+++ b/tests/automotive/health/multiuser/tests/Android.bp
@@ -41,6 +41,6 @@ android_test {
],
srcs: ["src/**/*.java"],
certificate: "platform",
- test_suites: ["catbox"],
+ test_suites: ["catbox", "ats"],
privileged: true,
}
diff --git a/tests/automotive/health/multiuser/tests/AndroidTest.xml b/tests/automotive/health/multiuser/tests/AndroidTest.xml
new file mode 100644
index 000000000..8fb13db04
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<configuration description="Runs Android Automotive Multiuser Scenario-Based Tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="AndroidAutomotiveMultiuserScenarioTests.apk" />
+ </target_preparer>
+
+ <!-- Switch to User 0 and wait for a some time (milliseconds) until system idle -->
+ <target_preparer class="com.android.tradefed.targetprep.SwitchUserTargetPreparer" >
+ <option name="user-type" value="SYSTEM" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.platform.scenario.multiuser" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/utils/shell-as/Android.bp b/utils/shell-as/Android.bp
new file mode 100644
index 000000000..96dc1c9d5
--- /dev/null
+++ b/utils/shell-as/Android.bp
@@ -0,0 +1,107 @@
+// Copyright (C) 2023 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.
+
+cc_binary {
+ name: "shell-as",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wextra",
+ ],
+ srcs: [
+ "*.cpp",
+ ":shell-as-test-app-apk-cpp",
+ ],
+ header_libs: ["libcutils_headers"],
+ static_executable: true,
+ static_libs: [
+ "libbase",
+ "libcap",
+ "liblog",
+ "libseccomp_policy",
+ "libselinux",
+ ],
+ arch: {
+ arm: {
+ srcs: ["shell-code/*-arm.S"]
+ },
+ arm64: {
+ srcs: ["shell-code/*-arm64.S"]
+ },
+ x86: {
+ srcs: ["shell-code/*-x86.S"]
+ },
+ x86_64: {
+ srcs: ["shell-code/*-x86_64.S"]
+ }
+ }
+}
+
+// A simple app that requests all non-system permissions and contains no other
+// functionality. This can be used as a target for shell-as to emulate the
+// security context of the most privileged possible non-system app.
+android_app {
+ name: "shell-as-test-app",
+ manifest: ":shell-as-test-app-manifest",
+ srcs: ["app/**/*.java"],
+ sdk_version: "9",
+ certificate: ":shell-as-test-app-cert",
+}
+
+// https://source.android.com/docs/core/ota/sign_builds#release-keys
+// Generated by running:
+// $ANDROID_BUILD_TOP/development/tools/make_key \
+// shell-as-test-app-key \
+// '/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com
+android_app_certificate {
+ name: "shell-as-test-app-cert",
+ certificate: "shell-as-test-app-key",
+}
+
+genrule {
+ name: "shell-as-test-app-manifest",
+ srcs: [
+ ":permission-list-normal",
+ "AndroidManifest.xml.template"
+ ],
+ cmd: "$(location gen-manifest.sh) " +
+ "$(location AndroidManifest.xml.template) " +
+ "$(location :permission-list-normal) " +
+ "$(out)",
+ out: ["AndroidManifest.xml"],
+ tool_files: ["gen-manifest.sh"],
+}
+
+// A source file that contains the contents of the above shell-as-test-app APK
+// embedded as an array.
+cc_genrule {
+ name: "shell-as-test-app-apk-cpp",
+ srcs: [":shell-as-test-app"],
+ cmd: "(" +
+ " echo '#include <stddef.h>';" +
+ " echo '#include <stdint.h>';" +
+ " echo '';" +
+ " echo 'namespace shell_as {';" +
+ " echo 'const uint8_t kTestAppApk[] = {';" +
+ " $(location toybox) xxd -i < $(in);" +
+ " echo '};';" +
+ " echo 'void GetTestApk(uint8_t **apk, size_t *length) {';" +
+ " echo ' *apk = (uint8_t*) kTestAppApk;';" +
+ " echo ' *length = sizeof(kTestAppApk);';" +
+ " echo '}';" +
+ " echo '} // namespace shell_as';" +
+ ") > $(out)",
+ out: ["test-app-apk.cpp"],
+ tools: ["toybox"]
+}
diff --git a/utils/shell-as/AndroidManifest.xml.template b/utils/shell-as/AndroidManifest.xml.template
new file mode 100644
index 000000000..07e89b186
--- /dev/null
+++ b/utils/shell-as/AndroidManifest.xml.template
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 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.
+-->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.google.tools.security.shell_as">
+
+ PERMISSIONS
+
+ <application
+ android:allowBackup="true"
+ android:label="Shell-As Test App">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/utils/shell-as/OWNERS b/utils/shell-as/OWNERS
new file mode 100644
index 000000000..431db9920
--- /dev/null
+++ b/utils/shell-as/OWNERS
@@ -0,0 +1,4 @@
+# Code owners for shell-as
+
+willcoster@google.com
+cdombroski@google.com
diff --git a/utils/shell-as/README.md b/utils/shell-as/README.md
new file mode 100644
index 000000000..e0f6f93f2
--- /dev/null
+++ b/utils/shell-as/README.md
@@ -0,0 +1,33 @@
+# shell-as
+
+shell-as is a utility that can be used to execute a binary in a less privileged
+security context. This can be useful for verifying the capabilities of a process
+on a running device or testing PoCs with different privilege levels.
+
+## Usage
+
+The security context can either be supplied explicitly, inferred from a process
+running on the device, or set to a predefined profile.
+
+For example, the following are equivalent and execute `/system/bin/id` in the
+context of the init process.
+
+```shell
+shell-as \
+ --uid 0 \
+ --gid 0 \
+ --selinux u:r:init:s0 \
+ --seccomp system \
+ /system/bin/id
+```
+
+```shell
+shell-as --pid 1 /system/bin/id
+```
+
+The "untrusted-app" profile can be used to execute a binary with all the
+possible privileges attainable by an untrusted app:
+
+```shell
+shell-as --profile untrusted-app /system/bin/id
+```
diff --git a/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java b/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java
new file mode 100644
index 000000000..d5d178c2f
--- /dev/null
+++ b/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.google.tools.security.shell_as;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/** An empty activity for the shell-as test app. */
+public class MainActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+}
diff --git a/utils/shell-as/command-line.cpp b/utils/shell-as/command-line.cpp
new file mode 100644
index 000000000..9a893c375
--- /dev/null
+++ b/utils/shell-as/command-line.cpp
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2023 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 "./command-line.h"
+
+#include <getopt.h>
+
+#include <iostream>
+#include <string>
+
+#include "./context.h"
+#include "./string-utils.h"
+
+namespace shell_as {
+
+namespace {
+const std::string kUsage =
+ R"(Usage: shell-as [options] [<program> <arguments>...]
+
+shell-as executes a program in a specified Android security context. The default
+program that is executed if none is specified is `/bin/system/sh`.
+
+The following options can be used to define the target security context.
+
+--verbose, -v Enables verbose logging.
+--uid <uid>, -u <uid> The target real and effective user ID.
+--gid <gid>, -g <gid> The target real and effective group ID.
+--groups <gid1,2,..>, -G <1,2,..> A comma separated list of supplementary group
+ IDs.
+--nogroups Specifies that all supplementary groups should
+ be cleared.
+--selinux <context>, -s <context> The target SELinux context.
+--seccomp <filter>, -f <filter> The target seccomp filter. Valid values of
+ filter are 'none', 'uid-inferred', 'app',
+ 'app-zygote', and 'system'.
+--caps <capabilities> A libcap textual expression that describes
+ the desired capability sets. The only
+ capability set that matters is the permitted
+ set, the other sets are ignored.
+
+ Examples:
+
+ "=" - Clear all capabilities
+ "=p" - Raise all capabilities
+ "23,CAP_SYS_ADMIN+p" - Raise CAP_SYS_ADMIN
+ and capability 23.
+
+ For a full description of the possible values
+ see `man 3 cap_from_text` (the libcap-dev
+ package provides this man page).
+--pid <pid>, -p <pid> Infer the target security context from a
+ running process with the given process ID.
+ This option implies --seccomp uid_inferred.
+ This option infers the capability from the
+ target process's permitted capability set.
+--profile <profile>, -P <profile> Infer the target security context from a
+ predefined security profile. Using this
+ option will install and execute a test app on
+ the device. Currently, the only valid profile
+ is 'untrusted-app' which corresponds to an
+ untrusted app which has been granted every
+ non-system permission.
+
+Options are evaluated in the order that they are given. For example, the
+following will set the target context to that of process 1234 but override the
+user ID to 0:
+
+ shell-as --pid 1234 --uid 0
+)";
+
+const char* kShellExecvArgs[] = {"/system/bin/sh", nullptr};
+
+bool ParseGroups(char* line, std::vector<gid_t>* ids) {
+ // Allow a null line as a valid input since this method is used to handle both
+ // --groups and --nogroups.
+ if (line == nullptr) {
+ return true;
+ }
+ return SplitIdsAndSkip(line, ",", /*num_to_skip=*/0, ids);
+}
+} // namespace
+
+bool ParseOptions(const int argc, char* const argv[], bool* verbose,
+ SecurityContext* context, char* const* execv_args[]) {
+ char short_options[] = "+s:hp:u:g:G:f:c:vP:";
+ struct option long_options[] = {
+ {"selinux", true, nullptr, 's'}, {"help", false, nullptr, 'h'},
+ {"uid", true, nullptr, 'u'}, {"gid", true, nullptr, 'g'},
+ {"pid", true, nullptr, 'p'}, {"verbose", false, nullptr, 'v'},
+ {"groups", true, nullptr, 'G'}, {"nogroups", false, nullptr, 'G'},
+ {"seccomp", true, nullptr, 'f'}, {"caps", true, nullptr, 'c'},
+ {"profile", true, nullptr, 'P'},
+ };
+ int option;
+ bool infer_seccomp_filter = false;
+ SecurityContext working_context;
+ std::vector<gid_t> supplementary_group_ids;
+ uint32_t working_id = 0;
+ while ((option = getopt_long(argc, argv, short_options, long_options,
+ nullptr)) != -1) {
+ switch (option) {
+ case 'v':
+ *verbose = true;
+ break;
+ case 'h':
+ std::cerr << kUsage;
+ return false;
+ case 'u':
+ if (!StringToUInt32(optarg, &working_id)) {
+ return false;
+ }
+ working_context.user_id = working_id;
+ break;
+ case 'g':
+ if (!StringToUInt32(optarg, &working_id)) {
+ return false;
+ }
+ working_context.group_id = working_id;
+ break;
+ case 'c':
+ working_context.capabilities = cap_from_text(optarg);
+ if (working_context.capabilities.value() == nullptr) {
+ std::cerr << "Unable to parse capabilities" << std::endl;
+ return false;
+ }
+ break;
+ case 'G':
+ supplementary_group_ids.clear();
+ if (!ParseGroups(optarg, &supplementary_group_ids)) {
+ std::cerr << "Unable to parse supplementary groups" << std::endl;
+ return false;
+ }
+ working_context.supplementary_group_ids = supplementary_group_ids;
+ break;
+ case 's':
+ working_context.selinux_context = optarg;
+ break;
+ case 'f':
+ infer_seccomp_filter = false;
+ if (strcmp(optarg, "uid-inferred") == 0) {
+ infer_seccomp_filter = true;
+ } else if (strcmp(optarg, "app") == 0) {
+ working_context.seccomp_filter = kAppFilter;
+ } else if (strcmp(optarg, "app-zygote") == 0) {
+ working_context.seccomp_filter = kAppZygoteFilter;
+ } else if (strcmp(optarg, "system") == 0) {
+ working_context.seccomp_filter = kSystemFilter;
+ } else if (strcmp(optarg, "none") == 0) {
+ working_context.seccomp_filter.reset();
+ } else {
+ std::cerr << "Invalid value for --seccomp: " << optarg << std::endl;
+ return false;
+ }
+ break;
+ case 'p':
+ if (!SecurityContextFromProcess(atoi(optarg), &working_context)) {
+ return false;
+ }
+ infer_seccomp_filter = true;
+ break;
+ case 'P':
+ if (strcmp(optarg, "untrusted-app") == 0) {
+ if (!SecurityContextFromTestApp(&working_context)) {
+ return false;
+ }
+ } else {
+ std::cerr << "Invalid value for --profile: " << optarg << std::endl;
+ return false;
+ }
+ infer_seccomp_filter = true;
+ break;
+ default:
+ std::cerr << "Unknown option '" << (char)optopt << "'" << std::endl;
+ return false;
+ }
+ }
+
+ if (infer_seccomp_filter) {
+ if (!working_context.user_id.has_value()) {
+ std::cerr << "No user ID; unable to infer appropriate seccomp filter."
+ << std::endl;
+ return false;
+ }
+ working_context.seccomp_filter =
+ SeccompFilterFromUserId(working_context.user_id.value());
+ }
+
+ *context = working_context;
+ if (optind < argc) {
+ *execv_args = argv + optind;
+ } else {
+ *execv_args = (char**)kShellExecvArgs;
+ }
+ return true;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/command-line.h b/utils/shell-as/command-line.h
new file mode 100644
index 000000000..4bf495f42
--- /dev/null
+++ b/utils/shell-as/command-line.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_COMMAND_LINE_H_
+#define SHELL_AS_COMMAND_LINE_H_
+
+#include "./context.h"
+
+namespace shell_as {
+
+// Parse command line options into a target security context and arguments that
+// can be passed to ExecuteInContext.
+//
+// The value of execv_args will either point to a sub-array of argv or to a
+// statically allocated default value. In both cases the caller should /not/
+// free the memory.
+//
+// Returns true on success and false if there is a problem parsing options.
+bool ParseOptions(const int argc, char* const argv[], bool* verbose,
+ SecurityContext* context, char* const* execv_args[]);
+} // namespace shell_as
+
+#endif // SHELL_AS_COMMAND_LINE_H_
diff --git a/utils/shell-as/context.cpp b/utils/shell-as/context.cpp
new file mode 100644
index 000000000..ea7979bab
--- /dev/null
+++ b/utils/shell-as/context.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 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 "./context.h"
+
+#include <private/android_filesystem_config.h> // For AID_APP_START.
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <iostream>
+#include <string>
+
+#include "./string-utils.h"
+#include "./test-app.h"
+
+namespace shell_as {
+
+namespace {
+
+bool ParseIdFromProcStatusLine(char* line, uid_t* id) {
+ // The user and group ID lines of the status file look like:
+ //
+ // Uid: <real> <effective> <saved> <filesystem>
+ // Gid: <real> <effective> <saved> <filesystem>
+ std::vector<uid_t> ids;
+ if (!SplitIdsAndSkip(line, "\t\n ", /*num_to_skip=*/1, &ids) ||
+ ids.size() < 1) {
+ return false;
+ }
+ *id = ids[0];
+ return true;
+}
+
+bool ParseGroupsFromProcStatusLine(char* line, std::vector<gid_t>* ids) {
+ // The supplementary groups line of the status file looks like:
+ //
+ // Groups: <group1> <group2> <group3> ...
+ return SplitIdsAndSkip(line, "\t\n ", /*num_to_skip=*/1, ids);
+}
+
+bool ParseProcStatusFile(const pid_t process_id, uid_t* real_user_id,
+ gid_t* real_group_id,
+ std::vector<gid_t>* supplementary_group_ids) {
+ std::string proc_status_path =
+ std::string("/proc/") + std::to_string(process_id) + "/status";
+ FILE* status_file = fopen(proc_status_path.c_str(), "r");
+ if (status_file == nullptr) {
+ std::cerr << "Unable to open '" << proc_status_path << "'" << std::endl;
+ }
+ bool parsed_user = false;
+ bool parsed_group = false;
+ bool parsed_supplementary_groups = false;
+ while (true) {
+ size_t line_length = 0;
+ char* line = nullptr;
+ if (getline(&line, &line_length, status_file) < 0) {
+ free(line);
+ break;
+ }
+ if (strncmp("Uid:", line, 4) == 0) {
+ parsed_user = ParseIdFromProcStatusLine(line, real_user_id);
+ } else if (strncmp("Gid:", line, 4) == 0) {
+ parsed_group = ParseIdFromProcStatusLine(line, real_group_id);
+ } else if (strncmp("Groups:", line, 7) == 0) {
+ parsed_supplementary_groups =
+ ParseGroupsFromProcStatusLine(line, supplementary_group_ids);
+ }
+ free(line);
+ }
+ fclose(status_file);
+ return parsed_user && parsed_group && parsed_supplementary_groups;
+}
+
+} // namespace
+
+bool SecurityContextFromProcess(const pid_t process_id,
+ SecurityContext* context) {
+ char* selinux_context;
+ if (getpidcon(process_id, &selinux_context) != 0) {
+ std::cerr << "Unable to obtain SELinux context from process " << process_id
+ << std::endl;
+ return false;
+ }
+
+ cap_t capabilities = cap_get_pid(process_id);
+ if (capabilities == nullptr) {
+ std::cerr << "Unable to obtain capability set from process " << process_id
+ << std::endl;
+ return false;
+ }
+
+ uid_t user_id = 0;
+ gid_t group_id = 0;
+ std::vector<gid_t> supplementary_group_ids;
+ if (!ParseProcStatusFile(process_id, &user_id, &group_id,
+ &supplementary_group_ids)) {
+ std::cerr << "Unable to obtain user and group IDs from process "
+ << process_id << std::endl;
+ return false;
+ }
+
+ context->selinux_context = selinux_context;
+ context->user_id = user_id;
+ context->group_id = group_id;
+ context->supplementary_group_ids = supplementary_group_ids;
+ context->capabilities = capabilities;
+ return true;
+}
+
+bool SecurityContextFromTestApp(SecurityContext* context) {
+ pid_t test_app_pid = 0;
+ if (!SetupAndStartTestApp(&test_app_pid)) {
+ std::cerr << "Unable to install test app." << std::endl;
+ return false;
+ }
+ return SecurityContextFromProcess(test_app_pid, context);
+}
+
+SeccompFilter SeccompFilterFromUserId(uid_t user_id) {
+ // Copied from:
+ // frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
+ return user_id >= AID_APP_START ? kAppFilter : kSystemFilter;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/context.h b/utils/shell-as/context.h
new file mode 100644
index 000000000..17a8cca85
--- /dev/null
+++ b/utils/shell-as/context.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_CONTEXT_H_
+#define SHELL_AS_CONTEXT_H_
+
+#include <selinux/selinux.h>
+#include <sys/capability.h>
+
+#include <memory>
+#include <optional>
+#include <vector>
+
+namespace shell_as {
+
+// Enumeration of the possible seccomp filters that Android may apply to a
+// process.
+//
+// This should be kept in sync with the policies defined in:
+// bionic/libc/seccomp/include/seccomp_policy.h
+enum SeccompFilter {
+ kAppFilter = 0,
+ kAppZygoteFilter = 1,
+ kSystemFilter = 2,
+};
+
+typedef struct SecurityContext {
+ std::optional<uid_t> user_id;
+ std::optional<gid_t> group_id;
+ std::optional<std::vector<gid_t>> supplementary_group_ids;
+ std::optional<char *> selinux_context;
+ std::optional<SeccompFilter> seccomp_filter;
+ std::optional<cap_t> capabilities;
+} SecurityContext;
+
+// Infers the appropriate seccomp filter from a user ID.
+//
+// This mimics the behavior of the zygote process and provides a sane default
+// method of picking a filter. However, it is not 100% accurate since it does
+// not assign the app zygote filter and would not return an appropriate value
+// for processes not started by the zygote.
+SeccompFilter SeccompFilterFromUserId(uid_t user_id);
+
+// Derives a complete security context from a given process.
+//
+// If unable to determine any field of the context this method will return false
+// and not modify the given context.
+bool SecurityContextFromProcess(pid_t process_id, SecurityContext* context);
+
+// Derives a complete security context from the bundled test app.
+//
+// If unable to determine any field of the context this method will return false
+// and not modify the given context.
+bool SecurityContextFromTestApp(SecurityContext* context);
+
+} // namespace shell_as
+
+#endif // SHELL_AS_CONTEXT_H_
diff --git a/utils/shell-as/elf-utils.cpp b/utils/shell-as/elf-utils.cpp
new file mode 100644
index 000000000..8a82555be
--- /dev/null
+++ b/utils/shell-as/elf-utils.cpp
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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 <elf.h>
+#include <stdio.h>
+
+#include <iostream>
+#include <string>
+
+#include "./elf.h"
+
+namespace shell_as {
+
+namespace {
+// The base address of a PIE binary when loaded with ASLR disabled.
+#if defined(__arm__) || defined(__aarch64__)
+constexpr uint64_t k32BitImageBase = 0xAAAAA000;
+constexpr uint64_t k64BitImageBase = 0x5555555000;
+#else
+constexpr uint64_t k32BitImageBase = 0x56555000;
+constexpr uint64_t k64BitImageBase = 0x555555554000;
+#endif
+} // namespace
+
+bool GetElfEntryPoint(const pid_t process_id, uint64_t* entry_address,
+ bool* is_arm_mode) {
+ uint8_t elf_header_buffer[sizeof(Elf64_Ehdr)];
+ std::string exe_path = "/proc/" + std::to_string(process_id) + "/exe";
+ FILE* exe_file = fopen(exe_path.c_str(), "rb");
+ if (exe_file == nullptr) {
+ std::cerr << "Unable to open executable of process " << process_id
+ << std::endl;
+ return false;
+ }
+
+ int read_size =
+ fread(elf_header_buffer, sizeof(elf_header_buffer), 1, exe_file);
+ fclose(exe_file);
+ if (read_size <= 0) {
+ std::cerr << "Unable to read executable of process " << process_id
+ << std::endl;
+ return false;
+ }
+
+ const Elf32_Ehdr* file_header_32 = (Elf32_Ehdr*)elf_header_buffer;
+ const Elf64_Ehdr* file_header_64 = (Elf64_Ehdr*)elf_header_buffer;
+ // The first handful of bytes of a header do not depend on whether the file is
+ // 32bit vs 64bit.
+ const bool is_pie_binary = file_header_32->e_type == ET_DYN;
+
+ if (file_header_32->e_ident[EI_CLASS] == ELFCLASS32) {
+ *entry_address =
+ file_header_32->e_entry + (is_pie_binary ? k32BitImageBase : 0);
+ } else if (file_header_32->e_ident[EI_CLASS] == ELFCLASS64) {
+ *entry_address =
+ file_header_64->e_entry + (is_pie_binary ? k64BitImageBase : 0);
+ } else {
+ return false;
+ }
+
+ *is_arm_mode = false;
+#if defined(__arm__)
+ if ((*entry_address & 1) == 0) {
+ *is_arm_mode = true;
+ }
+ // The entry address for ARM Elf binaries is branched to using a BX
+ // instruction. The low bit of these instructions indicates the instruction
+ // set of the code that is being jumped to. A low bit of 1 indicates thumb
+ // mode while a low bit of 0 indicates ARM mode.
+ *entry_address &= ~1;
+#endif
+
+ return true;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/elf-utils.h b/utils/shell-as/elf-utils.h
new file mode 100644
index 000000000..eba40f303
--- /dev/null
+++ b/utils/shell-as/elf-utils.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_ELF_H_
+#define SHELL_AS_ELF_H_
+
+#include <sys/types.h>
+
+namespace shell_as {
+
+// Sets entry_address to the process's entry point.
+//
+// This method assumes that PIE binaries are executing with ADDR_NO_RANDOMIZE.
+//
+// The is_arm_mode flag is set to true IFF the architecture is 32bit ARM and the
+// expected instruction set for code located at the entry address is not-thumb.
+// It is false for all other cases.
+bool GetElfEntryPoint(const pid_t process_id, uint64_t* entry_address,
+ bool* is_arm_mode);
+} // namespace shell_as
+
+#endif // SHELL_AS_ELF_H_
diff --git a/utils/shell-as/execute.cpp b/utils/shell-as/execute.cpp
new file mode 100644
index 000000000..3ef529252
--- /dev/null
+++ b/utils/shell-as/execute.cpp
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2023 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 "./execute.h"
+
+#include <linux/securebits.h>
+#include <linux/uio.h>
+#include <seccomp_policy.h>
+#include <sys/capability.h>
+#include <sys/personality.h>
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <iostream>
+#include <memory>
+
+#include "./elf-utils.h"
+#include "./registers.h"
+#include "./shell-code.h"
+
+namespace shell_as {
+
+namespace {
+
+// Capabilities are implemented as a 64-bit bit-vector. Therefore the maximum
+// number of capabilities supported by a kernel is 64.
+constexpr cap_value_t kMaxCapabilities = 64;
+
+bool DropPreExecPrivileges(const shell_as::SecurityContext* context) {
+ // The ordering here is important:
+ // (1) The platform's seccomp filters disallow setresgiud, so it must come
+ // before the seccomp drop.
+ // (2) Adding seccomp filters must happen before setresuid because setresuid
+ // drops some capabilities which are required for seccomp.
+ if (context->group_id.has_value() &&
+ setresgid(context->group_id.value(), context->group_id.value(),
+ context->group_id.value()) != 0) {
+ std::cerr << "Unable to set group id: " << context->group_id.value()
+ << std::endl;
+ return false;
+ }
+ if (context->supplementary_group_ids.has_value() &&
+ setgroups(context->supplementary_group_ids.value().size(),
+ context->supplementary_group_ids.value().data()) != 0) {
+ std::cerr << "Unable to set supplementary groups." << std::endl;
+ return false;
+ }
+
+ if (context->seccomp_filter.has_value()) {
+ switch (context->seccomp_filter.value()) {
+ case shell_as::kAppFilter:
+ set_app_seccomp_filter();
+ break;
+ case shell_as::kAppZygoteFilter:
+ set_app_zygote_seccomp_filter();
+ break;
+ case shell_as::kSystemFilter:
+ set_system_seccomp_filter();
+ break;
+ }
+ }
+
+ // This must be set prior to setresuid, otherwise that call will drop the
+ // permitted set of capabilities.
+ if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) != 0) {
+ std::cerr << "Unable to set keep capabilities." << std::endl;
+ return false;
+ }
+
+ if (context->user_id.has_value() &&
+ setresuid(context->user_id.value(), context->user_id.value(),
+ context->user_id.value()) != 0) {
+ std::cerr << "Unable to set user id: " << context->user_id.value()
+ << std::endl;
+ return false;
+ }
+
+ // Capabilities must be reacquired after setresuid since it still modifies
+ // capabilities, but it leaves the permitted set intact.
+ if (context->capabilities.has_value()) {
+ // The first step is to raise all the capabilities possible in all sets
+ // including the inheritable set. This defines the superset of possible
+ // capabilities that can be passed on after calling execve.
+ //
+ // The reason that all capabilities are raised in the inheritable set is due
+ // to a limitation of libcap. libcap may not contain a capability definition
+ // for all capabilities supported by the kernel. If this occurs, it will
+ // silently ignore requests to raise unknown capabilities via cap_set_flag.
+ //
+ // However, when parsing a cap_t from a text value, libcap will treat "all"
+ // as all possible 64 capability bits as set.
+ cap_t all_capabilities = cap_from_text("all+pie");
+ if (cap_set_proc(all_capabilities) != 0) {
+ std::cerr << "Unable to raise inheritable capability set." << std::endl;
+ cap_free(all_capabilities);
+ return false;
+ }
+ cap_free(all_capabilities);
+
+ // The second step is to raise the /desired/ capability subset in the
+ // ambient capability set. These are the capabilities that will actually be
+ // passed to the process after execve.
+ if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) != 0) {
+ std::cerr << "Unable to clear ambient capabilities." << std::endl;
+ return false;
+ }
+ cap_t desired_capabilities = context->capabilities.value();
+ for (cap_value_t cap = 0; cap < kMaxCapabilities; cap++) {
+ // Skip capability values not supported by the kernel.
+ if (!CAP_IS_SUPPORTED(cap)) {
+ continue;
+ }
+ cap_flag_value_t value = CAP_CLEAR;
+ if (cap_get_flag(desired_capabilities, cap, CAP_PERMITTED, &value) == 0 &&
+ value == CAP_SET) {
+ if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0) != 0) {
+ std::cerr << "Unable to raise capability " << cap
+ << " in the ambient set." << std::endl;
+ return false;
+ }
+ }
+ }
+
+ // The final step is to raise the SECBIT_NOROOT flag. The kernel has special
+ // case logic that treats root calling execve differently than other users.
+ //
+ // By default all bits in the permitted set prior to calling execve will be
+ // raised after calling execve. This would ignore the work above and result
+ // in the process to have all capabilities.
+ //
+ // Setting the SECBIT_NOROOT disables this special casing for root and
+ // causes the kernel to treat it as any other UID.
+ int64_t secure_bits = prctl(PR_GET_SECUREBITS, 0, 0, 0, 0);
+ if (secure_bits < 0 ||
+ prctl(PR_SET_SECUREBITS, secure_bits | SECBIT_NOROOT, 0, 0, 0) != 0) {
+ std::cerr << "Unable to raise SECBIT_NOROOT." << std::endl;
+ return false;
+ }
+ }
+ return true;
+}
+
+uint8_t ReadChildByte(const pid_t process, const uintptr_t address) {
+ uintptr_t data = ptrace(PTRACE_PEEKDATA, process, address, nullptr);
+ return ((uint8_t*)&data)[0];
+}
+
+void WriteChildByte(const pid_t process, const uintptr_t address,
+ const uint8_t value) {
+ // This is not the most efficient way to write data to a process. However, it
+ // reduces code complexity of handling different word sizes and reading and
+ // writing memory that is not a multiple of the native word size.
+ uintptr_t data = ptrace(PTRACE_PEEKDATA, process, address, nullptr);
+ ((uint8_t*)&data)[0] = value;
+ ptrace(PTRACE_POKEDATA, process, address, data);
+}
+
+void ReadChildMemory(const pid_t process, uintptr_t process_address,
+ uint8_t* bytes, size_t byte_count) {
+ for (; byte_count != 0; byte_count--, bytes++, process_address++) {
+ *bytes = ReadChildByte(process, process_address);
+ }
+}
+
+void WriteChildMemory(const pid_t process, uintptr_t process_address,
+ uint8_t const* bytes, size_t byte_count) {
+ for (; byte_count != 0; byte_count--, bytes++, process_address++) {
+ WriteChildByte(process, process_address, *bytes);
+ }
+}
+
+// Executes shell code in a target process.
+//
+// The following assumptions are made:
+// * The process is currently being ptraced and that the process has already
+// stopped.
+// * The shell code will raise SIGSTOP when it has finished as signal that
+// control flow should be handed back to the original code.
+// * The shell code only alters registers and pushes values onto the stack.
+//
+// Execution is performed by overwriting the memory under the current
+// instruction pointer with the shell code. After the shell code signals
+// completion the original register state and memory are restored.
+//
+// If the above assumptions are met, then this function will leave the process
+// in a stopped state that is equivalent to the original state.
+bool ExecuteShellCode(const pid_t process, const uint8_t* shell_code,
+ const size_t shell_code_size) {
+ REGISTER_STRUCT registers;
+ struct iovec registers_iovec;
+ registers_iovec.iov_base = &registers;
+ registers_iovec.iov_len = sizeof(REGISTER_STRUCT);
+ ptrace(PTRACE_GETREGSET, process, 1, &registers_iovec);
+
+ std::unique_ptr<uint8_t[]> memory_backup(new uint8_t[shell_code_size]);
+ ReadChildMemory(process, PROGRAM_COUNTER(registers), memory_backup.get(),
+ shell_code_size);
+ WriteChildMemory(process, PROGRAM_COUNTER(registers), shell_code,
+ shell_code_size);
+
+ // Execute the shell code and wait for the signal that it has finished.
+ ptrace(PTRACE_CONT, process, NULL, NULL);
+ int status;
+ waitpid(process, &status, 0);
+ if (status >> 8 != SIGSTOP) {
+ std::cerr << "Failed to execute SELinux shellcode." << std::endl;
+ return false;
+ }
+
+ ptrace(PTRACE_SETREGSET, process, 1, &registers_iovec);
+ WriteChildMemory(process, PROGRAM_COUNTER(registers), memory_backup.get(),
+ shell_code_size);
+ return true;
+}
+
+bool SetProgramCounter(const pid_t process_id, uint64_t program_counter) {
+ REGISTER_STRUCT registers;
+ struct iovec registers_iovec;
+ registers_iovec.iov_base = &registers;
+ registers_iovec.iov_len = sizeof(REGISTER_STRUCT);
+ if (ptrace(PTRACE_GETREGSET, process_id, 1, &registers_iovec) != 0) {
+ return false;
+ }
+ PROGRAM_COUNTER(registers) = program_counter;
+ if ((ptrace(PTRACE_SETREGSET, process_id, 1, &registers_iovec)) != 0) {
+ return false;
+ }
+ return true;
+}
+
+bool StepToEntryPoint(const pid_t process_id) {
+ bool is_arm_mode;
+ uint64_t entry_address;
+ if (!GetElfEntryPoint(process_id, &entry_address, &is_arm_mode)) {
+ std::cerr << "Not able to determine Elf entry point." << std::endl;
+ return false;
+ }
+ if (is_arm_mode) {
+ // TODO(willcoster): If there is a need to handle ARM mode instructions in
+ // addition to thumb instructions update this with ARM mode shell code.
+ std::cerr << "Attempting to run an ARM-mode binary. "
+ << "shell-as currently only supports thumb-mode. "
+ << "Bug willcoster@ if you run into this error." << std::endl;
+ return false;
+ }
+
+ int expected_signal = 0;
+ size_t trap_code_size = 0;
+ std::unique_ptr<uint8_t[]> trap_code =
+ GetTrapShellCode(&expected_signal, &trap_code_size);
+ std::unique_ptr<uint8_t[]> backup(new uint8_t[trap_code_size]);
+
+ // Set a break point at the entry point declared by the Elf file. When a
+ // statically linked binary is executed this will be the first instruction
+ // executed.
+ //
+ // When a dynamically linked binary is executed, the dynamic linker is
+ // executed first. This brings .so files into memory and resolves shared
+ // symbols. Once this process is finished, it jumps to the entry point
+ // declared in the Elf file.
+ ReadChildMemory(process_id, entry_address, backup.get(), trap_code_size);
+ WriteChildMemory(process_id, entry_address, trap_code.get(), trap_code_size);
+ ptrace(PTRACE_CONT, process_id, NULL, NULL);
+ int status;
+ waitpid(process_id, &status, 0);
+ if (status >> 8 != expected_signal) {
+ std::cerr << "Program exited unexpectedly while stepping to entry point."
+ << std::endl;
+ std::cerr << "Expected status " << expected_signal << " but encountered "
+ << (status >> 8) << std::endl;
+ return false;
+ }
+
+ if (!SetProgramCounter(process_id, entry_address)) {
+ return false;
+ }
+ WriteChildMemory(process_id, entry_address, backup.get(), trap_code_size);
+ return true;
+}
+
+} // namespace
+
+bool ExecuteInContext(char* const executable_and_args[],
+ const shell_as::SecurityContext* context) {
+ // Getting an executable running in a lower privileged context is tricky with
+ // SELinux. The recommended approach in the documentation is to use setexeccon
+ // which sets the context on the next execve call.
+ //
+ // However, this doesn't work for unprivileged processes like untrusted apps
+ // in Android because they are not allowed to execute most binaries.
+ //
+ // To work around this, ptrace is used to inject shell code into the new
+ // process just after it has executed an execve syscall. This shell code then
+ // sets the desired SELinux context.
+ pid_t child = fork();
+ if (child == 0) {
+ // Disabling ASLR makes it easier to determine the entry point of the target
+ // executable.
+ personality(ADDR_NO_RANDOMIZE);
+
+ // Drop the privileges that can be dropped before executing the new binary
+ // and exit early if there is an issue.
+ if (!DropPreExecPrivileges(context)) {
+ exit(1);
+ }
+
+ ptrace(PTRACE_TRACEME, 0, NULL, NULL);
+ raise(SIGSTOP); // Wait for the parent process to attach.
+ execv(executable_and_args[0], executable_and_args);
+ } else {
+ // Wait for the child to reach the SIGSTOP line above.
+ int status;
+ waitpid(child, &status, 0);
+ if ((status >> 8) != SIGSTOP) {
+ // If the first status is not SIGSTOP, then the child aborted early
+ // because it was not able to set the user and group IDs.
+ return false;
+ }
+
+ // Break inside the child's execv call.
+ ptrace(PTRACE_SETOPTIONS, child, NULL,
+ PTRACE_O_TRACEEXEC | PTRACE_O_EXITKILL);
+ ptrace(PTRACE_CONT, child, NULL, NULL);
+ waitpid(child, &status, 0);
+ if (status >> 8 != (SIGTRAP | PTRACE_EVENT_EXEC << 8)) {
+ std::cerr << "Failed to execute " << executable_and_args[0] << std::endl;
+ return false;
+ }
+
+ // Allow the dynamic linker to run before dropping to a lower SELinux
+ // context. This is required for executing in some very constrained domains
+ // like mediacodec.
+ //
+ // If the context was dropped before the dynamic linker runs, then when the
+ // linker attempts to read /proc/self/exe to determine dynamic symbol
+ // information, SELinux will kill the binary if the domain is not allowed to
+ // read the binary's executable file.
+ //
+ // This happens for example, when attempting to run any toybox binary (id,
+ // sh, etc) as mediacodec.
+ if (!StepToEntryPoint(child)) {
+ std::cerr << "Something bad happened stepping to the entry point."
+ << std::endl;
+ return false;
+ }
+
+ // Run the SELinux shellcode in the child process before the child can
+ // execute any instructions in the newly loaded executable.
+ if (context->selinux_context.has_value()) {
+ size_t shell_code_size;
+ std::unique_ptr<uint8_t[]> shell_code = GetSELinuxShellCode(
+ context->selinux_context.value(), &shell_code_size);
+ bool success = ExecuteShellCode(child, shell_code.get(), shell_code_size);
+ if (!success) {
+ return false;
+ }
+ }
+
+ // Resume and detach from the child now that the SELinux context has been
+ // updated.
+ ptrace(PTRACE_DETACH, child, NULL, NULL);
+ waitpid(child, nullptr, 0);
+ }
+ return true;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/execute.h b/utils/shell-as/execute.h
new file mode 100644
index 000000000..2e8f51189
--- /dev/null
+++ b/utils/shell-as/execute.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_EXECUTE_H_
+#define SHELL_AS_EXECUTE_H_
+
+#include "context.h"
+
+namespace shell_as {
+
+// Executes a command in the given security context.
+//
+// The executable_and_args parameter must contain at least two values. The first
+// value is the path to the executable to run and the last value must be null.
+// Additional arguments are passed to the executable as command line options.
+//
+// Returns true if the executable was run and false otherwise.
+bool ExecuteInContext(char* const executable_and_args[],
+ const SecurityContext* context);
+} // namespace shell_as
+
+#endif // SHELL_AS_EXECUTE_H_
diff --git a/utils/shell-as/gen-manifest.sh b/utils/shell-as/gen-manifest.sh
new file mode 100755
index 000000000..9dc4d1142
--- /dev/null
+++ b/utils/shell-as/gen-manifest.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+# Copyright (C) 2023 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.
+
+# Generates an AndroidManifest.xml file from a template by replacing the line
+# containing the substring, 'PERMISSIONS', with a list of permissions defined in
+# another text file.
+
+set -e
+
+if [ "$#" != 3 ];
+then
+ echo "usage: gen-manifest.sh AndroidManifest.xml.template" \
+ "permissions.txt AndroidManifest.xml"
+ exit 1
+fi
+
+readonly template="$1"
+readonly permissions="$2"
+readonly output="$3"
+
+echo "template = $1"
+
+# Print the XML template file before the line containing PERMISSIONS.
+sed -e '/PERMISSIONS/,$d' "$template" > "$output"
+
+# Print the permissions formatted as XML.
+sed -r 's!(.*)! <uses-permission android:name="\1"/>!g' "$permissions" >> "$output"
+
+# Print the XML template file after the line containing PERMISSIONS.
+sed -e '1,/PERMISSIONS/d' "$template" >> "$output"
diff --git a/utils/shell-as/registers.h b/utils/shell-as/registers.h
new file mode 100644
index 000000000..6f7af6c54
--- /dev/null
+++ b/utils/shell-as/registers.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_REGISTERS_H_
+#define SHELL_AS_REGISTERS_H_
+
+#if defined(__aarch64__)
+
+#define REGISTER_STRUCT struct user_pt_regs
+#define PROGRAM_COUNTER(regs) (regs.pc)
+
+#elif defined(__i386__)
+
+#include "sys/user.h"
+#define REGISTER_STRUCT struct user_regs_struct
+#define PROGRAM_COUNTER(regs) (regs.eip)
+
+#elif defined(__x86_64__)
+
+#include "sys/user.h"
+#define REGISTER_STRUCT struct user_regs_struct
+#define PROGRAM_COUNTER(regs) (regs.rip)
+
+#elif defined(__arm__)
+
+#define REGISTER_STRUCT struct user_regs
+#define PROGRAM_COUNTER(regs) (regs.ARM_pc)
+
+#endif
+
+#endif // SHELL_AS_REGISTERS_H_
diff --git a/utils/shell-as/shell-as-main.cpp b/utils/shell-as/shell-as-main.cpp
new file mode 100644
index 000000000..880cf1c91
--- /dev/null
+++ b/utils/shell-as/shell-as-main.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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 <iostream>
+#include <memory>
+#include <string>
+
+#include "./command-line.h"
+#include "./context.h"
+#include "./execute.h"
+
+int main(const int argc, char* const argv[]) {
+ bool verbose = false;
+ auto context = std::make_unique<shell_as::SecurityContext>();
+ char* const* execute_arguments = nullptr;
+ if (!shell_as::ParseOptions(argc, argv, &verbose, context.get(),
+ &execute_arguments)) {
+ return 1;
+ }
+
+ if (verbose) {
+ std::cerr << "Dropping privileges to:" << std::endl;
+ std::cerr << "\tuser ID = "
+ << (context->user_id.has_value()
+ ? std::to_string(context->user_id.value())
+ : "<no value>")
+ << std::endl;
+
+ std::cerr << "\tgroup ID = "
+ << (context->group_id.has_value()
+ ? std::to_string(context->group_id.value())
+ : "<no value>")
+ << std::endl;
+
+ std::cerr << "\tsupplementary group IDs = ";
+ if (!context->supplementary_group_ids.has_value()) {
+ std::cerr << "<no value>";
+ } else {
+ for (auto& id : context->supplementary_group_ids.value()) {
+ std::cerr << id << " ";
+ }
+ }
+ std::cerr << std::endl;
+
+ std::cerr << "\tSELinux = "
+ << (context->selinux_context.has_value()
+ ? context->selinux_context.value()
+ : "<no value>")
+ << std::endl;
+
+ std::cerr << "\tseccomp = ";
+ if (!context->seccomp_filter.has_value()) {
+ std::cerr << "<no value>";
+ } else {
+ switch (context->seccomp_filter.value()) {
+ case shell_as::kAppFilter:
+ std::cerr << "app";
+ break;
+ case shell_as::kAppZygoteFilter:
+ std::cerr << "app-zygote";
+ break;
+ case shell_as::kSystemFilter:
+ std::cerr << "system";
+ break;
+ }
+ }
+ std::cerr << std::endl;
+
+ std::cerr << "\tcapabilities = ";
+ if (!context->capabilities.has_value()) {
+ std::cerr << "<no value>";
+ } else {
+ std::cerr << "'" << cap_to_text(context->capabilities.value(), nullptr)
+ << "'";
+ }
+ std::cerr << std::endl;
+ }
+
+ return !shell_as::ExecuteInContext(execute_arguments, context.get());
+}
diff --git a/utils/shell-as/shell-as-test-app-key.pk8 b/utils/shell-as/shell-as-test-app-key.pk8
new file mode 100644
index 000000000..df9254543
--- /dev/null
+++ b/utils/shell-as/shell-as-test-app-key.pk8
Binary files differ
diff --git a/utils/shell-as/shell-as-test-app-key.x509.pem b/utils/shell-as/shell-as-test-app-key.x509.pem
new file mode 100644
index 000000000..4e5efc980
--- /dev/null
+++ b/utils/shell-as/shell-as-test-app-key.x509.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIECzCCAvOgAwIBAgIUNyI1+/ZDui4r+jp6uy/aVRBpeR0wDQYJKoZIhvcNAQEL
+BQAwgZQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMRAwDgYDVQQDDAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFu
+ZHJvaWQuY29tMB4XDTE5MTIwNTIyNDEwMloXDTQ3MDQyMjIyNDEwMlowgZQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFp
+biBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRyb2lkMRAwDgYD
+VQQDDAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29t
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyzHGxhfDk4VzImAGQyFV
+5+tb7Dgl9TTg57t8/LwMQX4abjB9o6tPwtZl757m4oLP8HCpjbI/kX5Wk4hLmNQ/
+I4AHG+LhCJGlz3nqBAJJxYoM//+3tUSLrq0ypuHMXNDPI5HGgE3hhzZbA9iWuGNB
+7in+bHuhPFUq8e5og6piy3s3f77GB8QXzJEKyO2FhQR1Do8t4UdRji7TWR+USHqw
+WBj/CyrpLJMwbr4Mx4YRN0JXUlFX1X/66ENonX4QZXofeiWDv5qgwFbbzgu9FLFN
+imDeeCzU0mtYEKQpmZOdEaWclkT8IzUPwPMdawEq3Wj8nutoma5CztYl+OO9BgJC
+LQIDAQABo1MwUTAdBgNVHQ4EFgQUqyxwI0Khq+xKbEGG3NCpN01wsaMwHwYDVR0j
+BBgwFoAUqyxwI0Khq+xKbEGG3NCpN01wsaMwDwYDVR0TAQH/BAUwAwEB/zANBgkq
+hkiG9w0BAQsFAAOCAQEAIx4k1g1jDQs3ekseXMvz0V+O9AArWOEmwkIcA6EISvfC
+dJ0DpmgRbZyvi0FowzOGYIZJ0Uwh4uwxETTHBQkvKoFdByukaasfX0p8axYVslT1
+87RrQDSA8fDp9K7d4kG3iXX16H5WJ0O/sI3UkZevZzVjXcoqSHA2CltGZv/EXPAh
+dwGL5OupiiJcCV4ISSgh9PHswH1tGASdg3nqFqQLZrCYZE3pyLdsiDTQADlBMpZ4
+dH7kbh8McSA/OM2Fp1y05oecYVzKOzJ/I4SLhbSGLRLHvSg9fNiJPoKm46leQtFV
+OVtzzBt6TKITRIhA8VVo45U0gVGUwlj/4BCKQLsJpA==
+-----END CERTIFICATE-----
diff --git a/utils/shell-as/shell-code.cpp b/utils/shell-as/shell-code.cpp
new file mode 100644
index 000000000..bdadf6c5a
--- /dev/null
+++ b/utils/shell-as/shell-code.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 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 "./shell-code.h"
+
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#define PAGE_START(addr) ((uintptr_t)addr & ~(PAGE_SIZE - 1))
+
+// Shell code that sets the SELinux context of the current process.
+//
+// The shell code expects a null-terminated SELinux context string to be placed
+// immediately after it in memory. After the SELinux context has been changed
+// the shell code will stop the current process with SIGSTOP.
+//
+// This shell code must be self-contained and position-independent.
+extern "C" void __setcon_shell_code_start();
+extern "C" void __setcon_shell_code_end();
+
+// Shell code that stops execution of the current process by raising a signal.
+// The specific signal that is raised is given in __trap_shell_code_signal.
+//
+// This shell code can be used to inject break points into a traced process.
+//
+// The shell code must not modify any registers other than the program counter.
+extern "C" void __trap_shell_code_start();
+extern "C" void __trap_shell_code_end();
+extern "C" int __trap_shell_code_signal;
+
+namespace shell_as {
+
+namespace {
+void EnsureShellcodeReadable(void (*start)(), void (*end)()) {
+ mprotect((void*)PAGE_START(start),
+ PAGE_START(end) - PAGE_START(start) + PAGE_SIZE,
+ PROT_READ | PROT_EXEC);
+}
+} // namespace
+
+std::unique_ptr<uint8_t[]> GetSELinuxShellCode(
+ char* selinux_context, size_t* total_size) {
+ EnsureShellcodeReadable(&__setcon_shell_code_start, &__setcon_shell_code_end);
+
+ size_t shell_code_size = (uintptr_t)&__setcon_shell_code_end -
+ (uintptr_t)&__setcon_shell_code_start;
+ size_t selinux_context_size = strlen(selinux_context) + 1 /* null byte */;
+ *total_size = shell_code_size + selinux_context_size;
+
+ std::unique_ptr<uint8_t[]> shell_code(new uint8_t[*total_size]);
+ memcpy(shell_code.get(), (void*)&__setcon_shell_code_start, shell_code_size);
+ memcpy(shell_code.get() + shell_code_size, selinux_context,
+ selinux_context_size);
+ return shell_code;
+}
+
+std::unique_ptr<uint8_t[]> GetTrapShellCode(int* expected_signal,
+ size_t* total_size) {
+ EnsureShellcodeReadable(&__trap_shell_code_start, &__trap_shell_code_end);
+
+ *expected_signal = __trap_shell_code_signal;
+
+ *total_size =
+ (uintptr_t)&__trap_shell_code_end - (uintptr_t)&__trap_shell_code_start;
+ std::unique_ptr<uint8_t[]> shell_code(new uint8_t[*total_size]);
+ memcpy(shell_code.get(), (void*)&__trap_shell_code_start, *total_size);
+ return shell_code;
+}
+} // namespace shell_as
diff --git a/utils/shell-as/shell-code.h b/utils/shell-as/shell-code.h
new file mode 100644
index 000000000..9c88d165a
--- /dev/null
+++ b/utils/shell-as/shell-code.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_SHELL_CODE_H_
+#define SHELL_AS_SHELL_CODE_H_
+
+#include <selinux/selinux.h>
+#include <sys/types.h>
+
+#include <memory>
+
+#include "context.h"
+
+namespace shell_as {
+
+// Returns shell code that when executed will set the current process's SElinux
+// context to the given value and then SIGSTOP itself.
+std::unique_ptr<uint8_t[]> GetSELinuxShellCode(
+ char* selinux_context, size_t* total_size);
+
+// Returns shell code that when executed will halt the current process and raise
+// a signal. The specific signal is returned in the expected_signal argument.
+std::unique_ptr<uint8_t[]> GetTrapShellCode(int* expected_signal,
+ size_t* total_size);
+} // namespace shell_as
+
+#endif // SHELL_AS_SHELL_CODE_H_
diff --git a/utils/shell-as/shell-code/constants-arm.S b/utils/shell-as/shell-code/constants-arm.S
new file mode 100644
index 000000000..10db630d9
--- /dev/null
+++ b/utils/shell-as/shell-code/constants-arm.S
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Arm specific constants.
+
+.equ SYS_OPEN, 0x000005
+.equ SYS_CLOSE, 0x000006
+.equ SYS_WRITE, 0x000004
+.equ SYS_KILL, 0x000025
+.equ SYS_GETPID, 0x000014
+.equ SYS_MPROTECT, 0x00007d
diff --git a/utils/shell-as/shell-code/constants-arm64.S b/utils/shell-as/shell-code/constants-arm64.S
new file mode 100644
index 000000000..a9dec758b
--- /dev/null
+++ b/utils/shell-as/shell-code/constants-arm64.S
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Arm64 specific constants.
+
+.equ SYS_OPENAT, 0x38
+.equ SYS_CLOSE, 0x39
+.equ SYS_WRITE, 0x40
+.equ SYS_KILL, 0x81
+.equ SYS_GETPID, 0xAC
+.equ SYS_MPROTECT, 0xE2
diff --git a/utils/shell-as/shell-code/constants-x86.S b/utils/shell-as/shell-code/constants-x86.S
new file mode 100644
index 000000000..afa9d1472
--- /dev/null
+++ b/utils/shell-as/shell-code/constants-x86.S
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// x86 specific constants.
+
+.equ SYS_WRITE, 0x04
+.equ SYS_OPEN, 0x05
+.equ SYS_CLOSE, 0x06
+.equ SYS_GETPID, 0x14
+.equ SYS_KILL, 0x25
+.equ SYS_MPROTECT, 0x7d
diff --git a/utils/shell-as/shell-code/constants-x86_64.S b/utils/shell-as/shell-code/constants-x86_64.S
new file mode 100644
index 000000000..0bf95cc75
--- /dev/null
+++ b/utils/shell-as/shell-code/constants-x86_64.S
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// x86-64 specific constants.
+
+.equ SYS_WRITE, 0x01
+.equ SYS_OPEN, 0x02
+.equ SYS_CLOSE, 0x03
+.equ SYS_GETPID, 0x27
+.equ SYS_KILL, 0x3e
+.equ SYS_MPROTECT, 0x0a
diff --git a/utils/shell-as/shell-code/constants.S b/utils/shell-as/shell-code/constants.S
new file mode 100644
index 000000000..9e2a238d5
--- /dev/null
+++ b/utils/shell-as/shell-code/constants.S
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Architecture independent constants.
+
+.equ AT_FDCWD, -100
+.equ O_WRONLY, 1
+
+// Possible memory access modes for mprotect.
+.equ PROT_NONE, 0
+.equ PROT_READ, 1
+.equ PROT_WRITE, 2
+.equ PROT_EXEC, 4
+
+.equ SIGILL, 4
+.equ SIGTRAP, 5
+.equ SIGSTOP, 19
diff --git a/utils/shell-as/shell-code/selinux-arm.S b/utils/shell-as/shell-code/selinux-arm.S
new file mode 100644
index 000000000..0c9480f9b
--- /dev/null
+++ b/utils/shell-as/shell-code/selinux-arm.S
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Shell code that sets the current SELinux context to a given string.
+//
+// The desired SELinux context is appended to the payload as a null-terminated
+// string.
+//
+// After the SELinux context has been updated the current process will raise
+// SIGSTOP.
+
+#include "./shell-code/constants.S"
+#include "./shell-code/constants-arm.S"
+
+.thumb
+
+.globl __setcon_shell_code_start
+.globl __setcon_shell_code_end
+
+__setcon_shell_code_start:
+ // Ensure that the context and SELinux /proc file are readable. This assumes
+ // that the max length of these two strings is shorter than 0x1000.
+ //
+ // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC)
+ mov r7, SYS_MPROTECT
+ adr r0, context
+ movw r2, 0xF000
+ movt r2, 0xFFFF
+ and r0, r0, r2
+ mov r1, 0x2000
+ mov r2, (PROT_READ | PROT_EXEC)
+ swi 0
+
+ // r10 = open("/proc/self/attr/current", O_WRONLY, O_WRONLY)
+ mov r7, SYS_OPEN
+ adr r0, selinux_proc_file
+ mov r1, O_WRONLY
+ mov r2, O_WRONLY
+ swi 0
+ mov r10, r0
+
+ // r11 = strlen(context)
+ mov r11, 0
+ adr r0, context
+strlen_start:
+ ldrb r1, [r0, r11]
+ cmp r1, 0
+ beq strlen_done
+ add r11, r11, 1
+ b strlen_start
+strlen_done:
+
+ // write(r10, context, r11)
+ mov r7, SYS_WRITE
+ mov r0, r10
+ adr r1, context
+ mov r2, r11
+ swi 0
+
+ // close(r10)
+ mov r7, SYS_CLOSE
+ mov r0, r10
+ swi 0
+
+ // r0 = getpid()
+ mov r7, SYS_GETPID
+ swi 0
+
+ // kill(r0, SIGSTOP)
+ mov r7, SYS_KILL
+ mov r1, SIGSTOP
+ swi 0
+
+selinux_proc_file:
+ .asciz "/proc/thread-self/attr/current"
+
+context:
+__setcon_shell_code_end:
diff --git a/utils/shell-as/shell-code/selinux-arm64.S b/utils/shell-as/shell-code/selinux-arm64.S
new file mode 100644
index 000000000..4e8c49296
--- /dev/null
+++ b/utils/shell-as/shell-code/selinux-arm64.S
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Shell code that sets the current SELinux context to a given string.
+//
+// The desired SELinux context is appended to the payload as a null-terminated
+// string.
+//
+// After the SELinux context has been updated the current process will raise
+// SIGSTOP.
+
+#include "./shell-code/constants.S"
+#include "./shell-code/constants-arm64.S"
+
+.globl __setcon_shell_code_start
+.globl __setcon_shell_code_end
+
+__setcon_shell_code_start:
+ // Ensure that the context and SELinux /proc file are readable. This assumes
+ // that the max length of these two strings is shorter than 0x1000.
+ //
+ // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC)
+ mov x8, SYS_MPROTECT
+ adr X0, __setcon_shell_code_end
+ and x0, x0, ~0xFFF
+ mov x1, 0x2000
+ mov x2, (PROT_READ | PROT_EXEC)
+ svc 0
+
+ // x10 = openat(AT_FDCWD, "/proc/self/attr/current", O_WRONLY, O_WRONLY)
+ mov x8, SYS_OPENAT
+ mov x0, AT_FDCWD
+ adr x1, selinux_proc_file
+ mov x2, O_WRONLY
+ mov x3, O_WRONLY
+ svc 0
+ mov x10, x0
+
+ // x11 = strlen(context)
+ mov x11, 0
+ adr x0, context
+strlen_start:
+ ldrb w1, [x0, x11]
+ cmp w1, 0
+ b.eq strlen_done
+ add x11, x11, 1
+ b strlen_start
+strlen_done:
+
+ // write(x10, context, x11)
+ mov x8, SYS_WRITE
+ mov x0, x10
+ adr x1, context
+ mov x2, x11
+ svc 0
+
+ // close(x10)
+ mov x8, SYS_CLOSE
+ mov x0, x10
+ svc 0
+
+ // x0 = getpid()
+ mov x8, SYS_GETPID
+ svc 0
+
+ // kill(x0, SIGSTOP)
+ mov x8, SYS_KILL
+ mov x1, SIGSTOP
+ svc 0
+
+selinux_proc_file:
+ .asciz "/proc/thread-self/attr/current"
+
+context:
+__setcon_shell_code_end:
diff --git a/utils/shell-as/shell-code/selinux-x86.S b/utils/shell-as/shell-code/selinux-x86.S
new file mode 100644
index 000000000..81c150f13
--- /dev/null
+++ b/utils/shell-as/shell-code/selinux-x86.S
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Shell code that sets the current SELinux context to a given string.
+//
+// The desired SELinux context is appended to the payload as a null-terminated
+// string.
+//
+// After the SELinux context has been updated the current process will raise
+// SIGSTOP.
+
+#include "./shell-code/constants.S"
+#include "./shell-code/constants-x86.S"
+
+.globl __setcon_shell_code_start
+.globl __setcon_shell_code_end
+
+__setcon_shell_code_start:
+
+ // x86 does not have RIP relative addressing. To work around this, relative
+ // calls are used to obtain the runtime address of a label. Once the location
+ // of one label is known, other labels can be addressed relative to the known
+ // label.
+ call constant_relative_address
+constant_relative_address:
+ pop %esi
+
+ // Ensure that the context and SELinux /proc file are readable. This assumes
+ // that the max length of these two strings is shorter than 0x1000.
+ //
+ // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC)
+ mov $SYS_MPROTECT, %eax
+ mov $~0xFFF, %ebx
+ and %esi, %ebx
+ mov $0x2000, %ecx
+ mov $(PROT_READ | PROT_EXEC), %edx
+ int $0x80
+
+ // ebx = open("/proc/self/attr/current", O_WRONLY, O_WRONLY)
+ mov $SYS_OPEN, %eax
+ lea (selinux_proc_file - constant_relative_address)(%esi), %ebx
+ mov $O_WRONLY, %ecx
+ mov $O_WRONLY, %edx
+ int $0x80
+ mov %eax, %ebx
+
+ // write(ebx, context, strlen(context))
+ xor %edx, %edx
+ leal (context - constant_relative_address)(%esi), %ecx
+strlen_start:
+ movb (%ecx, %edx), %al
+ test %al, %al
+ jz strlen_done
+ inc %edx
+ jmp strlen_start
+strlen_done:
+ mov $SYS_WRITE, %eax
+ int $0x80
+
+ // close(ebx)
+ mov $SYS_CLOSE, %eax
+ int $0x80
+
+ // ebx = getpid()
+ mov $SYS_GETPID, %eax
+ int $0x80
+ mov %eax, %ebx
+
+ // kill(ebx, SIGSTOP)
+ mov $SYS_KILL, %eax
+ mov $SIGSTOP, %ecx
+ int $0x80
+
+selinux_proc_file:
+ .asciz "/proc/self/attr/current"
+
+context:
+__setcon_shell_code_end:
diff --git a/utils/shell-as/shell-code/selinux-x86_64.S b/utils/shell-as/shell-code/selinux-x86_64.S
new file mode 100644
index 000000000..94fc876c6
--- /dev/null
+++ b/utils/shell-as/shell-code/selinux-x86_64.S
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Shell code that sets the current SELinux context to a given string.
+//
+// The desired SELinux context is appended to the payload as a null-terminated
+// string.
+//
+// After the SELinux context has been updated the current process will raise
+// SIGSTOP.
+
+#include "./shell-code/constants.S"
+#include "./shell-code/constants-x86_64.S"
+
+.globl __setcon_shell_code_start
+.globl __setcon_shell_code_end
+
+__setcon_shell_code_start:
+
+ // Ensure that the context and SELinux /proc file are readable. This assumes
+ // that the max length of these two strings is shorter than 0x1000.
+ //
+ // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC)
+ mov $SYS_MPROTECT, %rax
+ lea context(%rip), %rdi
+ and $~0xFFF, %rdi
+ mov $0x2000, %rsi
+ mov $(PROT_READ | PROT_EXEC), %rdx
+ syscall
+
+ // rdi = open("/proc/self/attr/current", O_WRONLY, O_WRONLY)
+ mov $SYS_OPEN, %eax
+ lea selinux_proc_file(%rip), %rdi
+ mov $O_WRONLY, %rsi
+ mov $O_WRONLY, %rdx
+ syscall
+ mov %rax, %rdi
+
+ // write(rdi, context, strlen(context))
+ xor %rdx, %rdx
+ lea context(%rip), %rsi
+strlen_start:
+ movb (%rsi, %rdx), %al
+ test %al, %al
+ jz strlen_done
+ inc %rdx
+ jmp strlen_start
+strlen_done:
+ mov $SYS_WRITE, %rax
+ syscall
+
+ // close(rdi)
+ mov $SYS_CLOSE, %rax
+ syscall
+
+ // rdi = getpid()
+ mov $SYS_GETPID, %rax
+ syscall
+ mov %rax, %rdi
+
+ // kill(rdi, SIGSTOP)
+ mov $SYS_KILL, %rax
+ mov $SIGSTOP, %rsi
+ syscall
+
+selinux_proc_file:
+ .asciz "/proc/self/attr/current"
+
+context:
+__setcon_shell_code_end:
diff --git a/utils/shell-as/shell-code/trap-arm.S b/utils/shell-as/shell-code/trap-arm.S
new file mode 100644
index 000000000..8bb347411
--- /dev/null
+++ b/utils/shell-as/shell-code/trap-arm.S
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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 "./shell-code/constants.S"
+
+.thumb
+
+.globl __trap_shell_code_start
+.globl __trap_shell_code_end
+.globl __trap_shell_code_signal
+
+__trap_shell_code_start:
+bkpt
+__trap_shell_code_end:
+
+__trap_shell_code_signal:
+.int SIGTRAP
diff --git a/utils/shell-as/shell-code/trap-arm64.S b/utils/shell-as/shell-code/trap-arm64.S
new file mode 100644
index 000000000..90063ffee
--- /dev/null
+++ b/utils/shell-as/shell-code/trap-arm64.S
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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 "./shell-code/constants.S"
+
+.globl __trap_shell_code_start
+.globl __trap_shell_code_end
+.globl __trap_shell_code_signal
+
+__trap_shell_code_start:
+hlt 0
+__trap_shell_code_end:
+
+__trap_shell_code_signal:
+.int SIGILL
diff --git a/utils/shell-as/shell-code/trap-x86.S b/utils/shell-as/shell-code/trap-x86.S
new file mode 100644
index 000000000..1669bb8ee
--- /dev/null
+++ b/utils/shell-as/shell-code/trap-x86.S
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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 "./shell-code/constants.S"
+
+.globl __trap_shell_code_start
+.globl __trap_shell_code_end
+.globl __trap_shell_code_signal
+
+__trap_shell_code_start:
+int $0x03
+__trap_shell_code_end:
+
+__trap_shell_code_signal:
+.int SIGTRAP
diff --git a/utils/shell-as/shell-code/trap-x86_64.S b/utils/shell-as/shell-code/trap-x86_64.S
new file mode 100644
index 000000000..1669bb8ee
--- /dev/null
+++ b/utils/shell-as/shell-code/trap-x86_64.S
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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 "./shell-code/constants.S"
+
+.globl __trap_shell_code_start
+.globl __trap_shell_code_end
+.globl __trap_shell_code_signal
+
+__trap_shell_code_start:
+int $0x03
+__trap_shell_code_end:
+
+__trap_shell_code_signal:
+.int SIGTRAP
diff --git a/utils/shell-as/string-utils.cpp b/utils/shell-as/string-utils.cpp
new file mode 100644
index 000000000..8977f73e0
--- /dev/null
+++ b/utils/shell-as/string-utils.cpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 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-utils.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+namespace shell_as {
+
+bool StringToUInt32(const char* s, uint32_t* i) {
+ uint64_t value = 0;
+ if (!StringToUInt64(s, &value)) {
+ return false;
+ }
+ if (value > UINT_MAX) {
+ return false;
+ }
+ *i = value;
+ return true;
+}
+
+bool StringToUInt64(const char* s, uint64_t* i) {
+ char* endptr = nullptr;
+ // Reset errno to a non-error value since strtoul does not clear errno.
+ errno = 0;
+ *i = strtoul(s, &endptr, 10);
+ // strtoul will return 0 if the value cannot be parsed as an unsigned long. If
+ // this occurs, ensure that the ID actually was zero. This is done by ensuring
+ // that the end pointer was advanced and that it now points to the end of the
+ // string (a null byte).
+ return errno == 0 && (*i != 0 || (endptr != s && *endptr == '\0'));
+}
+
+bool SplitIdsAndSkip(char* line, const char* separators, int num_to_skip,
+ std::vector<uid_t>* ids) {
+ if (line == nullptr) {
+ return false;
+ }
+
+ ids->clear();
+ for (char* id_string = strtok(line, separators); id_string != nullptr;
+ id_string = strtok(nullptr, separators)) {
+ if (num_to_skip > 0) {
+ num_to_skip--;
+ continue;
+ }
+
+ gid_t id;
+ if (!StringToUInt32(id_string, &id)) {
+ return false;
+ }
+ ids->push_back(id);
+ }
+ return true;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/string-utils.h b/utils/shell-as/string-utils.h
new file mode 100644
index 000000000..f4910894a
--- /dev/null
+++ b/utils/shell-as/string-utils.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_STRING_UTILS_H_
+#define SHELL_AS_STRING_UTILS_H_
+
+#include <unistd.h>
+
+#include <vector>
+
+namespace shell_as {
+
+// Parses a string into an unsigned 32bit int value. Returns true on success and
+// false otherwise.
+bool StringToUInt32(const char* s, uint32_t* i);
+
+// Parses a string into a unsigned 64bit int value. Returns true on success and
+// false otherwise.
+bool StringToUInt64(const char* s, uint64_t* i);
+
+// Splits a line of uid_t/guid_t values by a given separator and returns the
+// integer values in a vector.
+//
+// The separators string may contain multiple characters and is treated as a set
+// of possible separating characters.
+//
+// If num_to_skip is non-zero, then that many entries will be skipped after
+// splitting the line and before parsing the values as integers. This is useful
+// if the line has a prefix such as "Gid: 1 2 3 4".
+//
+// Returns true on success and false otherwise.
+bool SplitIdsAndSkip(char* line, const char* separators, int num_to_skip,
+ std::vector<uid_t>* ids);
+
+} // namespace shell_as
+
+#endif // SHELL_AS_STRING_UTILS_H_
diff --git a/utils/shell-as/test-app.cpp b/utils/shell-as/test-app.cpp
new file mode 100644
index 000000000..84fedcab6
--- /dev/null
+++ b/utils/shell-as/test-app.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 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 "./test-app.h"
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <iostream>
+#include <string>
+
+#include "./string-utils.h"
+
+namespace shell_as {
+
+// Returns a pointer to bytes of the test app APK along with the length in bytes
+// of the APK.
+//
+// This function is defined by the shell-as-test-app-apk-cpp genrule.
+void GetTestApk(uint8_t **apk, size_t *length);
+
+namespace {
+
+// The staging path for the test app APK.
+const char kTestAppApkStagingPath[] = "/data/local/tmp/shell-as-test-app.apk";
+
+// Writes the test app to a staging location and then installs the APK via the
+// 'pm' utility. The app is granted runtime permissions on installation. Returns
+// true if the app is installed successfully.
+bool InstallTestApp() {
+ uint8_t *apk = nullptr;
+ size_t apk_size = 0;
+ GetTestApk(&apk, &apk_size);
+
+ int staging_file = open(kTestAppApkStagingPath, O_WRONLY | O_CREAT | O_TRUNC,
+ S_IRUSR | S_IWUSR);
+ if (staging_file == -1) {
+ std::cerr << "Unable to open staging APK path." << std::endl;
+ return false;
+ }
+
+ size_t bytes_written = write(staging_file, apk, apk_size);
+ close(staging_file);
+ if (bytes_written != apk_size) {
+ std::cerr << "Unable to write entire test app APK." << std::endl;
+ return false;
+ }
+
+ const char cmd_template[] = "pm install -g %s > /dev/null 2> /dev/null";
+ char system_cmd[sizeof(cmd_template) + sizeof(kTestAppApkStagingPath) + 1] =
+ {};
+ sprintf(system_cmd, cmd_template, kTestAppApkStagingPath);
+ return system(system_cmd) == 0;
+}
+
+// Uninstalls the test app if it is installed. This method is a no-op if the app
+// is not installed.
+void UninstallTestApp() {
+ system(
+ "pm uninstall com.android.google.tools.security.shell_as"
+ " > /dev/null 2> /dev/null");
+}
+
+// Starts the main activity of the test app. This is necessary as some aspects
+// of the security context can only be inferred from a running process.
+bool StartTestApp() {
+ return system(
+ "am start-activity "
+ "com.android.google.tools.security.shell_as/"
+ ".MainActivity"
+ " > /dev/null 2> /dev/null") == 0;
+}
+
+// Obtain the process ID of the test app and returns true if it is running.
+// Returns false otherwise.
+bool GetTestAppProcessId(pid_t *test_app_pid) {
+ FILE *pgrep = popen(
+ "pgrep -f "
+ "com.android.google.tools.security.shell_as",
+ "r");
+ if (!pgrep) {
+ std::cerr << "Unable to execute pgrep." << std::endl;
+ return false;
+ }
+
+ char pgrep_output[128];
+ memset(pgrep_output, 0, sizeof(pgrep_output));
+ int bytes_read = fread(pgrep_output, 1, sizeof(pgrep_output) - 1, pgrep);
+ pclose(pgrep);
+ if (bytes_read <= 0) {
+ // Unable to find the process. This may happen if the app is still starting
+ // up.
+ return false;
+ }
+ return StringToUInt32(pgrep_output, (uint32_t *)test_app_pid);
+}
+} // namespace
+
+bool SetupAndStartTestApp(pid_t *test_app_pid) {
+ UninstallTestApp();
+
+ if (!InstallTestApp()) {
+ std::cerr << "Unable to install test app." << std::endl;
+ return false;
+ }
+
+ if (!StartTestApp()) {
+ std::cerr << "Unable to start and obtain test app PID." << std::endl;
+ return false;
+ }
+
+ for (int i = 0; i < 5; i++) {
+ if (GetTestAppProcessId(test_app_pid)) {
+ return true;
+ }
+ sleep(1);
+ }
+ return false;
+}
+} // namespace shell_as
diff --git a/utils/shell-as/test-app.h b/utils/shell-as/test-app.h
new file mode 100644
index 000000000..866bbfb33
--- /dev/null
+++ b/utils/shell-as/test-app.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_TEST_APP_H_
+#define SHELL_AS_TEST_APP_H_
+
+#include <sys/types.h>
+
+namespace shell_as {
+
+// Installs and launches the embedded shell-as test app. The test app requests
+// and is granted all non-system permissions defined by the OS. The test_app_pid
+// parameter is set to the process ID of the running test app. Returns true if
+// successful.
+bool SetupAndStartTestApp(pid_t *test_app_pid);
+}
+
+#endif // SHELL_AS_TEST_APP_H_