diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-15 09:36:19 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-15 09:36:19 +0000 |
commit | 3d936fdee35694f7baf0b52e4f6104168ca7aea6 (patch) | |
tree | df27762550944b33bbb1eabf6e257d4f6d5e3b1d | |
parent | cccf2c7a682045c0e26e5b95ef8400405f3a9854 (diff) | |
parent | 539a419cf93efa512a20c07c2a0e3fbf2bff8bc9 (diff) | |
download | platform_testing-aml_tz5_341510010.tar.gz |
Snap for 11224086 from 539a419cf93efa512a20c07c2a0e3fbf2bff8bc9 to mainline-tzdata5-releaseaml_tz5_341510050aml_tz5_341510010aml_tz5_341510010
Change-Id: Ie62f8c6f1e007a94b4cfec3b3086ba557ed3b22a
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 = ®isters; + registers_iovec.iov_len = sizeof(REGISTER_STRUCT); + ptrace(PTRACE_GETREGSET, process, 1, ®isters_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, ®isters_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 = ®isters; + registers_iovec.iov_len = sizeof(REGISTER_STRUCT); + if (ptrace(PTRACE_GETREGSET, process_id, 1, ®isters_iovec) != 0) { + return false; + } + PROGRAM_COUNTER(registers) = program_counter; + if ((ptrace(PTRACE_SETREGSET, process_id, 1, ®isters_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 Binary files differnew file mode 100644 index 000000000..df9254543 --- /dev/null +++ b/utils/shell-as/shell-as-test-app-key.pk8 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_ |