Add CTS tests for SELinux related change ids
Test that SELINUX_R_CHANGES and SELINUX_LATEST_CHANGES place an app in
the appropriate selinux domain. The R domain is currently the latest, so
test will need to be updated once that changes.
These tests are a more end-to-end variant of com.android.server.pm.SELinuxMMACTest.
Test: atest CompatChangesSelinuxTest
Bug: 168782947
Change-Id: I6f9ddce61d95f40687d9d8fa1ac87e95e72c38e1
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/Android.bp b/hostsidetests/appcompat/compatchanges/selinuxapp/Android.bp
new file mode 100644
index 0000000..472d554
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/Android.bp
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+java_library_static {
+ name: "selinux_app_empty",
+ sdk_version: "current",
+ srcs: ["src/**/*.java"],
+}
+
+android_test_helper_app {
+ name: "CtsSelinuxQCompatApp",
+ defaults: ["cts_defaults"],
+ sdk_version: "current",
+ static_libs: ["selinux_app_empty"],
+ manifest: "AndroidManifest_Q.xml",
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "vts10",
+ "general-tests",
+ ],
+}
+
+android_test_helper_app {
+ name: "CtsSelinuxRCompatApp",
+ defaults: ["cts_defaults"],
+ sdk_version: "current",
+ static_libs: ["selinux_app_empty"],
+ manifest: "AndroidManifest_R.xml",
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "vts10",
+ "general-tests",
+ ],
+}
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_Q.xml b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_Q.xml
new file mode 100644
index 0000000..5a6c1d3
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_Q.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.cts.appcompat.selinux_app">
+ <uses-sdk android:targetSdkVersion="29"/>
+ <application
+ android:debuggable="true">
+ <activity android:name=".Empty"
+ android:exported="true" />
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.cts.appcompat.selinux_app" />
+
+</manifest>
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_R.xml b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_R.xml
new file mode 100644
index 0000000..0fecd4f
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_R.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.cts.appcompat.selinux_app">
+ <uses-sdk android:targetSdkVersion="30"/>
+ <application
+ android:debuggable="true">
+ <activity android:name=".Empty"
+ android:exported="true" />
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.cts.appcompat.selinux_app" />
+
+</manifest>
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/src/com/android/cts/appcompat/selinux_app/Empty.java b/hostsidetests/appcompat/compatchanges/selinuxapp/src/com/android/cts/appcompat/selinux_app/Empty.java
new file mode 100644
index 0000000..a350bc0
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/src/com/android/cts/appcompat/selinux_app/Empty.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.appcompat.selinux_app;
+
+import android.app.Activity;
+
+public class Empty extends Activity {
+
+}
diff --git a/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesSelinuxTest.java b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesSelinuxTest.java
new file mode 100644
index 0000000..1006c47
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesSelinuxTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.appcompat;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Tests for the {@link android.app.compat.CompatChanges} SystemApi.
+ */
+
+public class CompatChangesSelinuxTest extends CompatChangeGatingTestCase {
+
+ protected static final String Q_TEST_APK = "CtsSelinuxQCompatApp.apk";
+ protected static final String R_TEST_APK = "CtsSelinuxRCompatApp.apk";
+
+ protected static final String TEST_PKG = "com.android.cts.appcompat.selinux_app";
+
+ private static final long SELINUX_LATEST_CHANGES = 143539591L;
+ private static final long SELINUX_R_CHANGES = 168782947L;
+
+ private static final Pattern PS_ENTRY_PATTERN = Pattern.compile("^(?<label>\\S+)\\s+(?<name>\\S+)");
+
+
+ public void testTargetSdkQAppIsInQDomainByDefault() throws Exception {
+ installPackage(Q_TEST_APK, false);
+ try {
+ startApp();
+ Map<String, String> packageToDomain = getPackageToDomain();
+
+ assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app_29");
+ } finally {
+ uninstallPackage(TEST_PKG, true);
+ }
+ }
+
+ public void testTargetSdkQAppIsInLatestDomainWithLatestOptin() throws Exception {
+ final Set<Long> enabledChanges = ImmutableSet.of(SELINUX_LATEST_CHANGES);
+ final Set<Long> disabledChanges = ImmutableSet.of();
+ final long configId = getClass().getCanonicalName().hashCode();
+
+ installPackage(Q_TEST_APK, false);
+ Thread.currentThread().sleep(100);
+ setCompatConfig(enabledChanges, disabledChanges, TEST_PKG);
+
+ try {
+ startApp();
+ Map<String, String> packageToDomain = getPackageToDomain();
+
+ assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+
+ } finally {
+ resetCompatConfig(TEST_PKG, enabledChanges, disabledChanges);
+ uninstallPackage(TEST_PKG, true);
+ }
+ }
+
+ public void testTargetSdkQAppIsInRDomainWithROptin() throws Exception {
+ final Set<Long> enabledChanges = ImmutableSet.of(SELINUX_R_CHANGES);
+ final Set<Long> disabledChanges = ImmutableSet.of();
+
+ installPackage(Q_TEST_APK, false);
+ Thread.currentThread().sleep(100);
+ setCompatConfig(enabledChanges, disabledChanges, TEST_PKG);
+
+ try {
+ startApp();
+ Map<String, String> packageToDomain = getPackageToDomain();
+ // TODO(b/168782947): Update domain if/when an R specific one is created to
+ // differentiate from untrusted_app.
+ assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+
+ } finally {
+ resetCompatConfig(TEST_PKG, enabledChanges, disabledChanges);
+ uninstallPackage(TEST_PKG, true);
+ }
+ }
+
+ public void testTargetSdkRAppIsInRDomainByDefault() throws Exception {
+ installPackage(R_TEST_APK, false);
+ try {
+ startApp();
+ Map<String, String> packageToDomain = getPackageToDomain();
+
+ assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+ } finally {
+ uninstallPackage(TEST_PKG, true);
+ }
+ }
+
+ public void testTargetSdkRAppIsInLatestDomainWithLatestOptin() throws Exception {
+ final Set<Long> enabledChanges = ImmutableSet.of(SELINUX_LATEST_CHANGES);
+ final Set<Long> disabledChanges = ImmutableSet.of();
+ installPackage(R_TEST_APK, false);
+ Thread.currentThread().sleep(100);
+ setCompatConfig(enabledChanges, disabledChanges, TEST_PKG);
+
+ try {
+ startApp();
+ Map<String, String> packageToDomain = getPackageToDomain();
+ assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+ } finally {
+ resetCompatConfig(TEST_PKG, enabledChanges, disabledChanges);
+ uninstallPackage(TEST_PKG, true);
+ }
+ }
+
+ private Map<String, String> getPackageToDomain() throws Exception {
+ Map<String, String> packageToDomain = new HashMap<>();
+ String output = getDevice().executeShellCommand("ps -e -o LABEL,NAME");
+ String[] lines = output.split("\n");
+ for (int i = 1; i < lines.length; ++i) {
+ String line = lines[i];
+ Matcher matcher = PS_ENTRY_PATTERN.matcher(line);
+ if (!matcher.matches())
+ continue;
+ String label = matcher.group("label");
+ String domain = label.split(":")[2];
+ String packageName = matcher.group("name");
+ packageToDomain.put(packageName, domain);
+ }
+ return packageToDomain;
+ }
+
+ private void startApp() throws Exception {
+ runCommand("am start -n " + TEST_PKG + "/" + TEST_PKG + ".Empty");
+ Thread.currentThread().sleep(100);
+ }
+
+
+}
diff --git a/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java b/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java
index 6fa2f2b..1217fbe 100644
--- a/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java
+++ b/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java
@@ -142,55 +142,53 @@
Set<Long> enabledChanges, Set<Long> disabledChanges,
Set<Long> reportedEnabledChanges, Set<Long> reportedDisabledChanges)
throws DeviceNotAvailableException {
+
// Set compat overrides
setCompatConfig(enabledChanges, disabledChanges, pkgName);
-
// Send statsd config
final long configId = getClass().getCanonicalName().hashCode();
createAndUploadStatsdConfig(configId, pkgName);
- // Run device-side test
- if (testClassName.startsWith(".")) {
- testClassName = pkgName + testClassName;
- }
- RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(pkgName, TEST_RUNNER,
- getDevice().getIDevice());
- testRunner.setMethodName(testClassName, testMethodName);
- CollectingTestListener listener = new CollectingTestListener();
- assertThat(getDevice().runInstrumentationTests(testRunner, listener)).isTrue();
-
- // Clear overrides.
- resetCompatChanges(enabledChanges, pkgName);
- resetCompatChanges(disabledChanges, pkgName);
-
- // Clear statsd report data and remove config
- Map<Long, Boolean> reportedChanges = getReportedChanges(configId, pkgName);
- removeStatsdConfig(configId);
-
- // Check that device side test occurred as expected
- final TestRunResult result = listener.getCurrentRunResults();
- assertWithMessage("Failed to successfully run device tests for %s: %s",
- result.getName(), result.getRunFailureMessage())
- .that(result.isRunFailure()).isFalse();
- assertWithMessage("Should run only exactly one test method!")
- .that(result.getNumTests()).isEqualTo(1);
- if (result.hasFailedTests()) {
- // build a meaningful error message
- StringBuilder errorBuilder = new StringBuilder("On-device test failed:\n");
- for (Map.Entry<TestDescription, TestResult> resultEntry :
- result.getTestResults().entrySet()) {
- if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
- errorBuilder.append(resultEntry.getKey().toString());
- errorBuilder.append(":\n");
- errorBuilder.append(resultEntry.getValue().getStackTrace());
- }
+ try {
+ // Run device-side test
+ if (testClassName.startsWith(".")) {
+ testClassName = pkgName + testClassName;
}
- throw new AssertionError(errorBuilder.toString());
+ RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(pkgName, TEST_RUNNER,
+ getDevice().getIDevice());
+ testRunner.setMethodName(testClassName, testMethodName);
+ CollectingTestListener listener = new CollectingTestListener();
+ assertThat(getDevice().runInstrumentationTests(testRunner, listener)).isTrue();
+
+ // Check that device side test occurred as expected
+ final TestRunResult result = listener.getCurrentRunResults();
+ assertWithMessage("Failed to successfully run device tests for %s: %s",
+ result.getName(), result.getRunFailureMessage())
+ .that(result.isRunFailure()).isFalse();
+ assertWithMessage("Should run only exactly one test method!")
+ .that(result.getNumTests()).isEqualTo(1);
+ if (result.hasFailedTests()) {
+ // build a meaningful error message
+ StringBuilder errorBuilder = new StringBuilder("On-device test failed:\n");
+ for (Map.Entry<TestDescription, TestResult> resultEntry :
+ result.getTestResults().entrySet()) {
+ if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+ errorBuilder.append(resultEntry.getKey().toString());
+ errorBuilder.append(":\n");
+ errorBuilder.append(resultEntry.getValue().getStackTrace());
+ }
+ }
+ throw new AssertionError(errorBuilder.toString());
+ }
+
+ } finally {
+ // Cleanup compat overrides
+ resetCompatConfig(pkgName, enabledChanges, disabledChanges);
+ // Validate statsd report
+ validatePostRunStatsdReport(configId, pkgName, reportedEnabledChanges,
+ reportedDisabledChanges);
}
- // Validate statsd report
- validatePostRunStatsdReport(reportedChanges, reportedEnabledChanges,
- reportedDisabledChanges);
}
/**
@@ -217,7 +215,7 @@
* @param pkgName The package name of the app that is expected to report the atom. It will be
* the only allowed log source.
*/
- private void createAndUploadStatsdConfig(long configId, String pkgName)
+ protected void createAndUploadStatsdConfig(long configId, String pkgName)
throws DeviceNotAvailableException {
final String atomName = "Atom" + System.nanoTime();
final String eventName = "Event" + System.nanoTime();
@@ -252,6 +250,8 @@
} catch (IOException e) {
throw new RuntimeException("IO error when writing to temp file.", e);
}
+ // Purge data
+ getReportList(configId);
}
/**
@@ -278,7 +278,7 @@
* @param disabledChanges Changes to be disabled.
* @param packageName Package name for the app whose config is being changed.
*/
- private void setCompatConfig(Set<Long> enabledChanges, Set<Long> disabledChanges,
+ protected void setCompatConfig(Set<Long> enabledChanges, Set<Long> disabledChanges,
@Nonnull String packageName) throws DeviceNotAvailableException {
for (Long enabledChange : enabledChanges) {
runCommand("am compat enable " + enabledChange + " " + packageName);
@@ -291,7 +291,7 @@
/**
* Reset changes to default for a package.
*/
- private void resetCompatChanges(Set<Long> changes, @Nonnull String packageName)
+ protected void resetCompatChanges(Set<Long> changes, @Nonnull String packageName)
throws DeviceNotAvailableException {
for (Long change : changes) {
runCommand("am compat reset " + change + " " + packageName);
@@ -332,16 +332,41 @@
}
/**
- * Validate that all overridden changes were logged while running the test.
+ * Cleanup the altered change ids under test.
+ *
+ * @param pkgName Package name of the app under test.
+ * @param enabledChanges Set of changes that were enabled during the test and need to be
+ * reset to the default value.
+ * @param disabledChanges Set of changes that were disabled during the test and need to
+ * be reset to the default value.
*/
- private void validatePostRunStatsdReport(Map<Long, Boolean> reportedChanges,
- Set<Long> enabledChanges, Set<Long> disabledChanges)
+ protected void resetCompatConfig( String pkgName, Set<Long> enabledChanges,
+ Set<Long> disabledChanges) throws DeviceNotAvailableException {
+ // Clear overrides.
+ resetCompatChanges(enabledChanges, pkgName);
+ resetCompatChanges(disabledChanges, pkgName);
+ }
+
+ /**
+ * Validate that all overridden changes were logged while running the test.
+ *
+ * @param configId The unique config id used to track change id queries.
+ * @param pkgName Package name of the app under test.
+ * @param loggedEnabledChanges Changes expected to be logged as enabled during the test.
+ * @param loggedDisabledChanges Changes expected to be logged as disabled during the test.
+ */
+ protected void validatePostRunStatsdReport(long configId, String pkgName,
+ Set<Long> loggedEnabledChanges, Set<Long> loggedDisabledChanges)
throws DeviceNotAvailableException {
- for (Long enabledChange : enabledChanges) {
+ // Clear statsd report data and remove config
+ Map<Long, Boolean> reportedChanges = getReportedChanges(configId, pkgName);
+ removeStatsdConfig(configId);
+
+ for (Long enabledChange : loggedEnabledChanges) {
assertThat(reportedChanges)
.containsEntry(enabledChange, true);
}
- for (Long disabledChange : disabledChanges) {
+ for (Long disabledChange : loggedDisabledChanges) {
assertThat(reportedChanges)
.containsEntry(disabledChange, false);
}