| /* |
| * Copyright (C) 2014 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.cts; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.platform.test.annotations.RestrictedBuildTest; |
| |
| import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; |
| import com.android.compatibility.common.tradefed.targetprep.DeviceInfoCollector; |
| import com.android.compatibility.common.util.CddTest; |
| import com.android.compatibility.common.util.PropertyUtil; |
| import com.android.tradefed.build.IBuildInfo; |
| import com.android.tradefed.device.CollectingOutputReceiver; |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; |
| import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; |
| import com.android.tradefed.util.FileUtil; |
| |
| import org.json.JSONObject; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| |
| import java.io.BufferedReader; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.nio.file.Files; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| |
| /** |
| * Host-side SELinux tests. |
| * |
| * These tests analyze the policy file in use on the subject device directly or |
| * run as the shell user to evaluate aspects of the state of SELinux on the test |
| * device which otherwise would not be available to a normal apk. |
| */ |
| @RunWith(DeviceJUnit4ClassRunner.class) |
| public class SELinuxHostTest extends BaseHostJUnit4Test { |
| |
| // Keep in sync with AndroidTest.xml |
| private static final String DEVICE_INFO_DEVICE_DIR = "/sdcard/device-info-files/"; |
| // Keep in sync with com.android.compatibility.common.deviceinfo.VintfDeviceInfo |
| private static final String VINTF_DEVICE_CLASS = "VintfDeviceInfo"; |
| // Keep in sync with |
| // com.android.compatibility.common.deviceinfo.DeviceInfo#testCollectDeviceInfo() |
| private static final String DEVICE_INFO_SUFFIX = ".deviceinfo.json"; |
| private static final String VINTF_DEVICE_JSON = VINTF_DEVICE_CLASS + DEVICE_INFO_SUFFIX; |
| // Keep in sync with com.android.compatibility.common.deviceinfo.VintfDeviceInfo |
| private static final String SEPOLICY_VERSION_JSON_KEY = "sepolicy_version"; |
| private static final String PLATFORM_SEPOLICY_VERSION_JSON_KEY = "platform_sepolicy_version"; |
| |
| private static final Map<ITestDevice, File> sCachedDevicePolicyFiles = new HashMap<>(1); |
| private static final Map<ITestDevice, File> sCachedDevicePlatFcFiles = new HashMap<>(1); |
| private static final Map<ITestDevice, File> sCachedDeviceVendorFcFiles = new HashMap<>(1); |
| private static final Map<ITestDevice, File> sCachedDeviceVendorManifest = new HashMap<>(1); |
| private static final Map<ITestDevice, File> sCachedDeviceVendorPolicy = new HashMap<>(1); |
| private static final Map<ITestDevice, File> sCachedDeviceVintfJson = new HashMap<>(1); |
| private static final Map<ITestDevice, File> sCachedDeviceSystemPolicy = new HashMap<>(1); |
| |
| private File mSepolicyAnalyze; |
| private File checkSeapp; |
| private File checkFc; |
| private File aospFcFile; |
| private File aospPcFile; |
| private File aospSvcFile; |
| private File devicePolicyFile; |
| private File deviceSystemPolicyFile; |
| private File devicePlatFcFile; |
| private File deviceVendorFcFile; |
| private File devicePcFile; |
| private File deviceSvcFile; |
| private File seappNeverAllowFile; |
| private File copyLibcpp; |
| private File sepolicyTests; |
| |
| private IBuildInfo mBuild; |
| |
| /** |
| * A reference to the device under test. |
| */ |
| private ITestDevice mDevice; |
| |
| public static File copyResourceToTempFile(String resName) throws IOException { |
| InputStream is = SELinuxHostTest.class.getResourceAsStream(resName); |
| String tempFileName = "SELinuxHostTest" + resName.replace("/", "_"); |
| File tempFile = createTempFile(tempFileName, ".tmp"); |
| FileOutputStream os = new FileOutputStream(tempFile); |
| byte[] buf = new byte[1024]; |
| int len; |
| |
| while ((len = is.read(buf)) != -1) { |
| os.write(buf, 0, len); |
| } |
| os.flush(); |
| os.close(); |
| return tempFile; |
| } |
| |
| private static void appendTo(String dest, String src) throws IOException { |
| try (FileInputStream is = new FileInputStream(new File(src)); |
| FileOutputStream os = new FileOutputStream(new File(dest))) { |
| byte[] buf = new byte[1024]; |
| int len; |
| |
| while ((len = is.read(buf)) != -1) { |
| os.write(buf, 0, len); |
| } |
| } |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| mDevice = getDevice(); |
| mBuild = getBuild(); |
| // Assumes every test in this file asserts a requirement of CDD section 9. |
| assumeSecurityModelCompat(); |
| |
| CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild); |
| mSepolicyAnalyze = copyResourceToTempFile("/sepolicy-analyze"); |
| mSepolicyAnalyze.setExecutable(true); |
| |
| devicePolicyFile = getDevicePolicyFile(mDevice); |
| if (isSepolicySplit(mDevice)) { |
| devicePlatFcFile = getDeviceFile(mDevice, sCachedDevicePlatFcFiles, |
| "/system/etc/selinux/plat_file_contexts", "plat_file_contexts"); |
| deviceVendorFcFile = getDeviceFile(mDevice, sCachedDeviceVendorFcFiles, |
| "/vendor/etc/selinux/vendor_file_contexts", "vendor_file_contexts"); |
| deviceSystemPolicyFile = |
| android.security.cts.SELinuxHostTest.getDeviceSystemPolicyFile(mDevice); |
| } else { |
| devicePlatFcFile = getDeviceFile(mDevice, sCachedDevicePlatFcFiles, |
| "/plat_file_contexts", "plat_file_contexts"); |
| deviceVendorFcFile = getDeviceFile(mDevice, sCachedDeviceVendorFcFiles, |
| "/vendor_file_contexts", "vendor_file_contexts"); |
| } |
| } |
| |
| @After |
| public void cleanUp() throws Exception { |
| mSepolicyAnalyze.delete(); |
| } |
| |
| private void assumeSecurityModelCompat() throws Exception { |
| // This feature name check only applies to devices that first shipped with |
| // SC or later. |
| final int firstApiLevel = Math.min(PropertyUtil.getFirstApiLevel(mDevice), |
| PropertyUtil.getVendorApiLevel(mDevice)); |
| if (firstApiLevel >= 31) { |
| assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.", |
| getDevice().hasFeature("feature:android.hardware.security.model.compatible")); |
| } |
| } |
| |
| /* |
| * IMPLEMENTATION DETAILS: We cache some host-side policy files on per-device basis (in case |
| * CTS supports running against multiple devices at the same time). HashMap is used instead |
| * of WeakHashMap because in the grand scheme of things, keeping ITestDevice and |
| * corresponding File objects from being garbage-collected is not a big deal in CTS. If this |
| * becomes a big deal, this can be switched to WeakHashMap. |
| */ |
| private static File getDeviceFile(ITestDevice device, |
| Map<ITestDevice, File> cache, String deviceFilePath, |
| String tmpFileName) throws Exception { |
| if (!device.doesFileExist(deviceFilePath)){ |
| throw new Exception("File not found on the device: " + deviceFilePath); |
| } |
| File file; |
| synchronized (cache) { |
| file = cache.get(device); |
| } |
| if (file != null) { |
| return file; |
| } |
| file = createTempFile(tmpFileName, ".tmp"); |
| device.pullFile(deviceFilePath, file); |
| synchronized (cache) { |
| cache.put(device, file); |
| } |
| return file; |
| } |
| |
| private static File buildSystemPolicy(ITestDevice device, Map<ITestDevice, File> cache, |
| String tmpFileName) throws Exception { |
| File builtPolicyFile; |
| synchronized (cache) { |
| builtPolicyFile = cache.get(device); |
| } |
| if (builtPolicyFile != null) { |
| return builtPolicyFile; |
| } |
| |
| builtPolicyFile = createTempFile(tmpFileName, ".tmp"); |
| |
| File secilc = copyResourceToTempFile("/secilc"); |
| secilc.setExecutable(true); |
| |
| File systemSepolicyCilFile = createTempFile("plat_sepolicy", ".cil"); |
| File fileContextsFile = createTempFile("file_contexts", ".txt"); |
| assertTrue(device.pullFile("/system/etc/selinux/plat_sepolicy.cil", systemSepolicyCilFile)); |
| |
| List<String> command = new ArrayList<>(Arrays.asList( |
| secilc.getAbsolutePath(), |
| "-m", |
| "-M", |
| "true", |
| "-c", |
| "30", |
| "-o", |
| builtPolicyFile.getAbsolutePath(), |
| "-f", |
| fileContextsFile.getAbsolutePath(), |
| systemSepolicyCilFile.getAbsolutePath())); |
| |
| File systemExtCilFile = createTempFile("system_ext_sepolicy", ".cil"); |
| File productCilFile = createTempFile("product_sepolicy", ".cil"); |
| if (device.pullFile("/system_ext/etc/selinux/system_ext_sepolicy.cil", systemExtCilFile)) { |
| command.add(systemExtCilFile.getAbsolutePath()); |
| } |
| if (device.pullFile("/product/etc/selinux/product_sepolicy.cil", productCilFile)) { |
| command.add(productCilFile.getAbsolutePath()); |
| } |
| |
| String errorString = tryRunCommand(command.toArray(new String[0])); |
| assertTrue(errorString, errorString.length() == 0); |
| |
| synchronized (cache) { |
| cache.put(device, builtPolicyFile); |
| } |
| return builtPolicyFile; |
| } |
| |
| private static File buildVendorPolicy(IBuildInfo build, ITestDevice device, |
| Map<ITestDevice, File> cache, String tmpFileName) throws Exception { |
| File builtPolicyFile; |
| synchronized (cache) { |
| builtPolicyFile = cache.get(device); |
| } |
| if (builtPolicyFile != null) { |
| return builtPolicyFile; |
| } |
| |
| builtPolicyFile = createTempFile(tmpFileName, ".tmp"); |
| |
| File secilc = copyResourceToTempFile("/secilc"); |
| secilc.setExecutable(true); |
| |
| int vendorVersion = getVendorSepolicyVersion(build, device); |
| |
| File platSepolicyFile = copyResourceToTempFile("/" + vendorVersion + "_plat_sepolicy.cil"); |
| File platMappingFile = copyResourceToTempFile("/" + vendorVersion + "_mapping.cil"); |
| File vendorSepolicyCilFile = createTempFile("vendor_sepolicy", ".cil"); |
| File platPubVersionedCilFile = createTempFile("plat_pub_versioned", ".cil"); |
| File odmSepolicyCilFile = createTempFile("odm_sepolicy", ".cil"); |
| File fileContextsFile = createTempFile("file_contexts", ".txt"); |
| |
| assertTrue(device.pullFile("/vendor/etc/selinux/vendor_sepolicy.cil", |
| vendorSepolicyCilFile)); |
| assertTrue(device.pullFile("/vendor/etc/selinux/plat_pub_versioned.cil", |
| platPubVersionedCilFile)); |
| |
| List<String> command = new ArrayList<>(Arrays.asList( |
| secilc.getAbsolutePath(), |
| "-m", |
| "-M", |
| "true", |
| "-c", |
| "30", |
| "-N", |
| "-o", |
| builtPolicyFile.getAbsolutePath(), |
| "-f", |
| fileContextsFile.getAbsolutePath(), |
| platSepolicyFile.getAbsolutePath(), |
| platMappingFile.getAbsolutePath(), |
| vendorSepolicyCilFile.getAbsolutePath(), |
| platPubVersionedCilFile.getAbsolutePath())); |
| |
| if (device.pullFile("/odm/etc/selinux/odm_sepolicy.cil", odmSepolicyCilFile)) { |
| command.add(odmSepolicyCilFile.getAbsolutePath()); |
| } |
| |
| String errorString = tryRunCommand(command.toArray(new String[0])); |
| assertTrue(errorString, errorString.length() == 0); |
| |
| synchronized (cache) { |
| cache.put(device, builtPolicyFile); |
| } |
| return builtPolicyFile; |
| } |
| |
| /** |
| * Returns the host-side file containing the SELinux policy of the device under test. |
| */ |
| public static File getDevicePolicyFile(ITestDevice device) throws Exception { |
| return getDeviceFile(device, sCachedDevicePolicyFiles, "/sys/fs/selinux/policy", |
| "sepolicy"); |
| } |
| |
| /** |
| * Returns the host-side file containing the system SELinux policy of the device under test. |
| */ |
| public static File getDeviceSystemPolicyFile(ITestDevice device) throws Exception { |
| return buildSystemPolicy(device, sCachedDeviceSystemPolicy, "system_sepolicy"); |
| } |
| |
| /** |
| * Returns the host-side file containing the vendor SELinux policy of the device under test. |
| */ |
| public static File getDeviceVendorPolicyFile(IBuildInfo build, ITestDevice device) |
| throws Exception { |
| return buildVendorPolicy(build, device, sCachedDeviceVendorPolicy, "vendor_sepolicy"); |
| } |
| |
| /** |
| * Returns the major number of sepolicy version of device's vendor implementation. |
| */ |
| public static int getVendorSepolicyVersion(IBuildInfo build, ITestDevice device) |
| throws Exception { |
| |
| // Try different methods to get vendor SEPolicy version in the following order: |
| // 1. Retrieve from IBuildInfo as stored by DeviceInfoCollector (relies on #2) |
| // 2. If it fails, retrieve from device info JSON file stored on the device |
| // (relies on android.os.VintfObject) |
| // 3. If it fails, retrieve from raw VINTF device manifest files by guessing its path on |
| // the device |
| // Usually, the method #1 should work. If it doesn't, fallback to method #2 and #3. If |
| // none works, throw the error from method #1. |
| Exception buildInfoEx; |
| try { |
| return getVendorSepolicyVersionFromBuildInfo(build); |
| } catch (Exception ex) { |
| CLog.e("getVendorSepolicyVersionFromBuildInfo failed: " + ex); |
| buildInfoEx = ex; |
| } |
| try { |
| return getVendorSepolicyVersionFromDeviceJson(device); |
| } catch (Exception ex) { |
| CLog.e("getVendorSepolicyVersionFromDeviceJson failed: " + ex); |
| } |
| try { |
| return getVendorSepolicyVersionFromManifests(device); |
| } catch (Exception ex) { |
| CLog.e("getVendorSepolicyVersionFromManifests failed: " + ex); |
| throw new Exception("Unable to get the vendor policy version from the device:", |
| buildInfoEx); |
| } |
| } |
| |
| /** |
| * Returns VSR (Vendor Software Requirements) api level. Returns 0 if the property |
| * ro.vendor.api_level doesn't exist |
| */ |
| private static int getVSRApiLevel(ITestDevice device) throws Exception { |
| try { |
| return Integer.parseInt(device.getProperty("ro.vendor.api_level")); |
| } catch (Exception ex) { |
| CLog.e("getProperty(\"ro.vendor.api_level\") failed: ", ex); |
| return 0; |
| } |
| } |
| |
| /** |
| * Retrieve the major number of sepolicy version from VINTF device info stored in the given |
| * IBuildInfo by {@link DeviceInfoCollector}. |
| */ |
| private static int getVendorSepolicyVersionFromBuildInfo(IBuildInfo build) throws Exception { |
| File deviceInfoDir = build.getFile(DeviceInfoCollector.DEVICE_INFO_DIR); |
| File vintfJson = deviceInfoDir.toPath().resolve(VINTF_DEVICE_JSON).toFile(); |
| return getVendorSepolicyVersionFromJsonFile(vintfJson); |
| } |
| |
| /** |
| * Retrieve the major number of sepolicy version from VINTF device info stored on the device by |
| * VintfDeviceInfo. |
| */ |
| private static int getVendorSepolicyVersionFromDeviceJson(ITestDevice device) throws Exception { |
| File vintfJson = getDeviceFile(device, sCachedDeviceVintfJson, |
| DEVICE_INFO_DEVICE_DIR + VINTF_DEVICE_JSON, VINTF_DEVICE_JSON); |
| return getVendorSepolicyVersionFromJsonFile(vintfJson); |
| } |
| |
| /** |
| * Retrieve the major number of sepolicy version from the given JSON string that contains VINTF |
| * device info. |
| */ |
| private static int getVendorSepolicyVersionFromJsonFile(File vintfJson) throws Exception { |
| String content = FileUtil.readStringFromFile(vintfJson); |
| JSONObject object = new JSONObject(content); |
| String version = object.getString(SEPOLICY_VERSION_JSON_KEY); |
| return getSepolicyVersionFromMajorMinor(version); |
| } |
| |
| /** |
| * Deprecated. |
| * Retrieve the major number of sepolicy version from raw device manifest XML files. |
| * Note that this is depends on locations of VINTF devices files at Android 10 and do not |
| * search new paths, hence this may not work on devices launching Android 11 and later. |
| */ |
| private static int getVendorSepolicyVersionFromManifests(ITestDevice device) throws Exception { |
| String deviceManifestPath = null; |
| |
| //check default path /vendor/etc/vintf/manifest.xml, prefer to use by default |
| if (device.doesFileExist("/vendor/etc/vintf/manifest.xml")) { |
| deviceManifestPath = "/vendor/etc/vintf/manifest.xml"; |
| } |
| |
| //only if /vendor/etc/vintf/manifest.xml not exist, then check /vendor/etc/vintf/manifest_{vendorSku}.xml |
| String vendorSku = device.getProperty("ro.boot.product.vendor.sku"); |
| if (deviceManifestPath == null && vendorSku != null && vendorSku.length() > 0) { |
| String vendorSkuDeviceManifestPath = "/vendor/etc/vintf/manifest_"+ vendorSku + ".xml"; |
| if (device.doesFileExist(vendorSkuDeviceManifestPath)) { |
| deviceManifestPath = vendorSkuDeviceManifestPath; |
| } |
| } |
| |
| //use /vendor/manifest.xml if above paths not exist |
| if (deviceManifestPath == null) { |
| deviceManifestPath = "/vendor/manifest.xml"; |
| } |
| |
| CLog.i("getVendorSepolicyVersionFromManifests " + deviceManifestPath); |
| |
| File vendorManifestFile = getDeviceFile(device, sCachedDeviceVendorManifest, |
| deviceManifestPath, "manifest.xml"); |
| |
| DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
| DocumentBuilder db = dbf.newDocumentBuilder(); |
| Document doc = db.parse(vendorManifestFile); |
| Element root = doc.getDocumentElement(); |
| Element sepolicy = (Element) root.getElementsByTagName("sepolicy").item(0); |
| Element version = (Element) sepolicy.getElementsByTagName("version").item(0); |
| return getSepolicyVersionFromMajorMinor(version.getTextContent()); |
| } |
| |
| /** |
| * Returns the major number of sepolicy version of system. |
| */ |
| public static int getSystemSepolicyVersion(IBuildInfo build) throws Exception { |
| File deviceInfoDir = build.getFile(DeviceInfoCollector.DEVICE_INFO_DIR); |
| File vintfJson = deviceInfoDir.toPath().resolve(VINTF_DEVICE_JSON).toFile(); |
| String content = FileUtil.readStringFromFile(vintfJson); |
| JSONObject object = new JSONObject(content); |
| String version = object.getString(PLATFORM_SEPOLICY_VERSION_JSON_KEY); |
| return getSepolicyVersionFromMajorMinor(version); |
| } |
| |
| /** |
| * Get the major number from an SEPolicy version string, e.g. "27.0" => 27. |
| */ |
| private static int getSepolicyVersionFromMajorMinor(String version) { |
| String sepolicyVersion = version.split("\\.")[0]; |
| return Integer.parseInt(sepolicyVersion); |
| } |
| |
| /** |
| * Tests that the kernel is enforcing selinux policy globally. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testGlobalEnforcing() throws Exception { |
| CollectingOutputReceiver out = new CollectingOutputReceiver(); |
| mDevice.executeShellCommand("cat /sys/fs/selinux/enforce", out); |
| assertEquals("SELinux policy is not being enforced!", "1", out.getOutput()); |
| } |
| |
| /** |
| * Tests that all domains in the running policy file are in enforcing mode |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @RestrictedBuildTest |
| @Test |
| public void testAllDomainsEnforcing() throws Exception { |
| |
| /* run sepolicy-analyze permissive check on policy file */ |
| String errorString = tryRunCommand(mSepolicyAnalyze.getAbsolutePath(), |
| devicePolicyFile.getAbsolutePath(), "permissive"); |
| assertTrue("The following SELinux domains were found to be in permissive mode:\n" |
| + errorString, errorString.length() == 0); |
| } |
| |
| /** |
| * Asserts that specified type is not associated with the specified |
| * attribute. |
| * |
| * @param attribute |
| * The attribute name. |
| * @param type |
| * The type name. |
| */ |
| private void assertNotInAttribute(String attribute, String badtype) throws Exception { |
| Set<String> actualTypes = sepolicyAnalyzeGetTypesAssociatedWithAttribute(attribute); |
| if (actualTypes.contains(badtype)) { |
| fail("Attribute " + attribute + " includes " + badtype); |
| } |
| } |
| |
| private static final byte[] readFully(InputStream in) throws IOException { |
| ByteArrayOutputStream result = new ByteArrayOutputStream(); |
| byte[] buf = new byte[65536]; |
| int chunkSize; |
| while ((chunkSize = in.read(buf)) != -1) { |
| result.write(buf, 0, chunkSize); |
| } |
| return result.toByteArray(); |
| } |
| |
| /** |
| * Runs sepolicy-analyze against the device's SELinux policy and returns the set of types |
| * associated with the provided attribute. |
| */ |
| private Set<String> sepolicyAnalyzeGetTypesAssociatedWithAttribute( |
| String attribute) throws Exception { |
| ProcessBuilder pb = |
| new ProcessBuilder( |
| mSepolicyAnalyze.getAbsolutePath(), |
| devicePolicyFile.getAbsolutePath(), |
| "attribute", |
| attribute); |
| pb.redirectOutput(ProcessBuilder.Redirect.PIPE); |
| pb.redirectErrorStream(true); |
| Process p = pb.start(); |
| int errorCode = p.waitFor(); |
| if (errorCode != 0) { |
| fail("sepolicy-analyze attribute " + attribute + " failed with error code " + errorCode |
| + ": " + new String(readFully(p.getInputStream()))); |
| } |
| try (BufferedReader in = |
| new BufferedReader(new InputStreamReader(p.getInputStream()))) { |
| Set<String> types = new HashSet<>(); |
| String type; |
| while ((type = in.readLine()) != null) { |
| types.add(type.trim()); |
| } |
| return types; |
| } |
| } |
| |
| /** |
| * Returns {@code true} if this device is required to be a full Treble device. |
| */ |
| public static boolean isFullTrebleDevice(ITestDevice device) |
| throws DeviceNotAvailableException { |
| return PropertyUtil.getFirstApiLevel(device) > 26 && |
| PropertyUtil.propertyEquals(device, "ro.treble.enabled", "true"); |
| } |
| |
| private boolean isFullTrebleDevice() throws DeviceNotAvailableException { |
| return isFullTrebleDevice(mDevice); |
| } |
| |
| /** |
| * Returns {@code true} if this device is required to enforce compatible property. |
| */ |
| public static boolean isCompatiblePropertyEnforcedDevice(ITestDevice device) |
| throws DeviceNotAvailableException { |
| return PropertyUtil.propertyEquals( |
| device, "ro.actionable_compatible_property.enabled", "true"); |
| } |
| |
| /** |
| * Returns {@code true} if this device has sepolicy split across different paritions. |
| * This is possible even for devices launched at api level higher than 26. |
| */ |
| public static boolean isSepolicySplit(ITestDevice device) |
| throws DeviceNotAvailableException { |
| return PropertyUtil.getFirstApiLevel(device) > 34 /* Build.VERSION_CODES.UPSIDE_DOWN_CAKE */ |
| || device.doesFileExist("/system/etc/selinux/plat_file_contexts"); |
| } |
| |
| /** |
| * Asserts that no HAL server domains are exempted from the prohibition of socket use with the |
| * only exceptions for the automotive device type. |
| */ |
| @Test |
| public void testNoExemptionsForSocketsUseWithinHalServer() throws Exception { |
| if (!isFullTrebleDevice()) { |
| return; |
| } |
| |
| if (getDevice().hasFeature("feature:android.hardware.type.automotive")) { |
| return; |
| } |
| |
| Set<String> types = sepolicyAnalyzeGetTypesAssociatedWithAttribute( |
| "hal_automotive_socket_exemption"); |
| if (!types.isEmpty()) { |
| List<String> sortedTypes = new ArrayList<>(types); |
| Collections.sort(sortedTypes); |
| fail("Policy exempts domains from ban on socket usage from HAL servers: " |
| + sortedTypes); |
| } |
| } |
| |
| /** |
| * Tests that mlstrustedsubject does not include untrusted_app |
| * and that mlstrustedobject does not include app_data_file. |
| * This helps prevent circumventing the per-user isolation of |
| * normal apps via levelFrom=user. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testMLSAttributes() throws Exception { |
| assertNotInAttribute("mlstrustedsubject", "untrusted_app"); |
| assertNotInAttribute("mlstrustedobject", "app_data_file"); |
| } |
| |
| /** |
| * Tests that the seapp_contexts file on the device is valid. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testValidSeappContexts() throws Exception { |
| /* obtain seapp_contexts file from running device |
| * |
| * PLEASE KEEP IN SYNC WITH: |
| * external/selinux/libselinux/src/android/android_seapp.c |
| */ |
| File platformSeappFile = createTempFile("plat_seapp_contexts", ".tmp"); |
| File systemExtSeappFile = createTempFile("system_ext_seapp_contexts", ".tmp"); |
| File productSeappFile = createTempFile("product_seapp_contexts", ".tmp"); |
| File vendorSeappFile = createTempFile("vendor_seapp_contexts", ".tmp"); |
| File odmSeappFile = createTempFile("odm_seapp_contexts", ".tmp"); |
| if (mDevice.pullFile("/system/etc/selinux/plat_seapp_contexts", platformSeappFile)) { |
| mDevice.pullFile("/system_ext/etc/selinux/system_ext_seapp_contexts", |
| systemExtSeappFile); |
| mDevice.pullFile("/product/etc/selinux/product_seapp_contexts", productSeappFile); |
| mDevice.pullFile("/vendor/etc/selinux/vendor_seapp_contexts", vendorSeappFile); |
| mDevice.pullFile("/odm/etc/selinux/odm_seapp_contexts", odmSeappFile); |
| } else { |
| mDevice.pullFile("/plat_seapp_contexts", platformSeappFile); |
| mDevice.pullFile("/system_ext_seapp_contexts", systemExtSeappFile); |
| mDevice.pullFile("/product_seapp_contexts", productSeappFile); |
| mDevice.pullFile("/vendor_seapp_contexts", vendorSeappFile); |
| mDevice.pullFile("/odm_seapp_contexts", odmSeappFile); |
| } |
| |
| /* retrieve the checkseapp executable from jar */ |
| checkSeapp = copyResourceToTempFile("/checkseapp"); |
| checkSeapp.setExecutable(true); |
| |
| /* retrieve the AOSP seapp_neverallows file from jar */ |
| seappNeverAllowFile = copyResourceToTempFile("/plat_seapp_neverallows"); |
| |
| /* run checkseapp on seapp_contexts */ |
| String errorString = tryRunCommand(checkSeapp.getAbsolutePath(), |
| "-p", devicePolicyFile.getAbsolutePath(), |
| seappNeverAllowFile.getAbsolutePath(), |
| platformSeappFile.getAbsolutePath(), |
| systemExtSeappFile.getAbsolutePath(), |
| productSeappFile.getAbsolutePath(), |
| vendorSeappFile.getAbsolutePath(), |
| odmSeappFile.getAbsolutePath()); |
| assertTrue("The seapp_contexts file was invalid:\n" |
| + errorString, errorString.length() == 0); |
| |
| /* run checkseapp on vendor contexts to find coredomain violations, starting from V */ |
| int vsrVersion = getVSRApiLevel(getDevice()); |
| if (vsrVersion > 34) /* V or later */ { |
| errorString = tryRunCommand(checkSeapp.getAbsolutePath(), |
| "-p", devicePolicyFile.getAbsolutePath(), |
| "-c", /* coredomain check */ |
| vendorSeappFile.getAbsolutePath(), |
| odmSeappFile.getAbsolutePath()); |
| assertTrue("vendor seapp_contexts contains coredomain:\n" |
| + errorString, errorString.length() == 0); |
| } |
| } |
| |
| /** |
| * Asserts that the actual file contains all the lines from the expected file. |
| * It does not guarantee the order of the lines. |
| * |
| * @param expectedFile |
| * The file with the expected contents. |
| * @param actualFile |
| * The actual file being checked. |
| */ |
| private void assertContainsAllLines(File expectedFile, File actualFile) throws Exception { |
| List<String> expectedLines = Files.readAllLines(expectedFile.toPath()); |
| List<String> actualLines = Files.readAllLines(actualFile.toPath()); |
| |
| expectedLines.replaceAll(String::trim); |
| actualLines.replaceAll(String::trim); |
| |
| HashSet<String> expected = new HashSet(expectedLines); |
| HashSet<String> actual = new HashSet(actualLines); |
| |
| /* remove all seen lines from expected, ignoring new entries */ |
| expected.removeAll(actual); |
| assertTrue("Line removed: " + String.join("\n", expected), expected.isEmpty()); |
| } |
| |
| /** |
| * Tests that the seapp_contexts file on the device contains |
| * the standard AOSP entries. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testAospSeappContexts() throws Exception { |
| |
| /* obtain seapp_contexts file from running device */ |
| File platformSeappFile = createTempFile("seapp_contexts", ".tmp"); |
| if (!mDevice.pullFile("/system/etc/selinux/plat_seapp_contexts", platformSeappFile)) { |
| mDevice.pullFile("/plat_seapp_contexts", platformSeappFile); |
| } |
| /* retrieve the AOSP seapp_contexts file from jar */ |
| File aospSeappFile = copyResourceToTempFile("/plat_seapp_contexts"); |
| |
| assertContainsAllLines(aospSeappFile, platformSeappFile); |
| } |
| |
| /** |
| * Tests that the plat_file_contexts file on the device contains |
| * the standard AOSP entries. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testAospFileContexts() throws Exception { |
| |
| /* retrieve the checkfc executable from jar */ |
| checkFc = copyResourceToTempFile("/checkfc"); |
| checkFc.setExecutable(true); |
| |
| /* retrieve the AOSP file_contexts file from jar */ |
| aospFcFile = copyResourceToTempFile("/plat_file_contexts"); |
| |
| /* run checkfc -c plat_file_contexts plat_file_contexts */ |
| String result = tryRunCommand(checkFc.getAbsolutePath(), |
| "-c", aospFcFile.getAbsolutePath(), |
| devicePlatFcFile.getAbsolutePath()).trim(); |
| assertTrue("The file_contexts file did not include the AOSP entries:\n" |
| + result + "\n", |
| result.equals("equal") || result.equals("subset")); |
| } |
| |
| /** |
| * Tests that the property_contexts file on the device contains |
| * the standard AOSP entries. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testAospPropertyContexts() throws Exception { |
| |
| /* obtain property_contexts file from running device */ |
| devicePcFile = createTempFile("plat_property_contexts", ".tmp"); |
| // plat_property_contexts may be either in /system/etc/sepolicy or in / |
| if (!mDevice.pullFile("/system/etc/selinux/plat_property_contexts", devicePcFile)) { |
| mDevice.pullFile("/plat_property_contexts", devicePcFile); |
| } |
| |
| // Retrieve the AOSP property_contexts file from JAR. |
| // The location of this file in the JAR has nothing to do with the location of this file on |
| // Android devices. See build script of this CTS module. |
| aospPcFile = copyResourceToTempFile("/plat_property_contexts"); |
| |
| assertContainsAllLines(aospPcFile, devicePcFile); |
| } |
| |
| /** |
| * Tests that the service_contexts file on the device contains |
| * the standard AOSP entries. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testAospServiceContexts() throws Exception { |
| |
| /* obtain service_contexts file from running device */ |
| deviceSvcFile = createTempFile("service_contexts", ".tmp"); |
| if (!mDevice.pullFile("/system/etc/selinux/plat_service_contexts", deviceSvcFile)) { |
| mDevice.pullFile("/plat_service_contexts", deviceSvcFile); |
| } |
| |
| /* retrieve the AOSP service_contexts file from jar */ |
| aospSvcFile = copyResourceToTempFile("/plat_service_contexts"); |
| |
| assertContainsAllLines(aospSvcFile, deviceSvcFile); |
| } |
| |
| /** |
| * Tests that the file_contexts file(s) on the device is valid. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testValidFileContexts() throws Exception { |
| |
| /* retrieve the checkfc executable from jar */ |
| checkFc = copyResourceToTempFile("/checkfc"); |
| checkFc.setExecutable(true); |
| |
| /* combine plat and vendor policies for testing */ |
| File combinedFcFile = createTempFile("combined_file_context", ".tmp"); |
| appendTo(combinedFcFile.getAbsolutePath(), devicePlatFcFile.getAbsolutePath()); |
| appendTo(combinedFcFile.getAbsolutePath(), deviceVendorFcFile.getAbsolutePath()); |
| |
| /* run checkfc sepolicy file_contexts */ |
| String errorString = tryRunCommand(checkFc.getAbsolutePath(), |
| devicePolicyFile.getAbsolutePath(), |
| combinedFcFile.getAbsolutePath()); |
| assertTrue("file_contexts was invalid:\n" |
| + errorString, errorString.length() == 0); |
| } |
| |
| /** |
| * Tests that the property_contexts file on the device is valid. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testValidPropertyContexts() throws Exception { |
| |
| /* retrieve the checkfc executable from jar */ |
| File propertyInfoChecker = copyResourceToTempFile("/property_info_checker"); |
| propertyInfoChecker.setExecutable(true); |
| |
| /* obtain property_contexts file from running device */ |
| devicePcFile = createTempFile("property_contexts", ".tmp"); |
| // plat_property_contexts may be either in /system/etc/sepolicy or in / |
| if (!mDevice.pullFile("/system/etc/selinux/plat_property_contexts", devicePcFile)) { |
| mDevice.pullFile("/plat_property_contexts", devicePcFile); |
| } |
| |
| /* run property_info_checker on property_contexts */ |
| String errorString = tryRunCommand(propertyInfoChecker.getAbsolutePath(), |
| devicePolicyFile.getAbsolutePath(), |
| devicePcFile.getAbsolutePath()); |
| assertTrue("The property_contexts file was invalid:\n" |
| + errorString, errorString.length() == 0); |
| } |
| |
| /** |
| * Tests that the service_contexts file on the device is valid. |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testValidServiceContexts() throws Exception { |
| |
| /* retrieve the checkfc executable from jar */ |
| checkFc = copyResourceToTempFile("/checkfc"); |
| checkFc.setExecutable(true); |
| |
| /* obtain service_contexts file from running device */ |
| deviceSvcFile = createTempFile("service_contexts", ".tmp"); |
| mDevice.pullFile("/service_contexts", deviceSvcFile); |
| |
| /* run checkfc -s on service_contexts */ |
| String errorString = tryRunCommand(checkFc.getAbsolutePath(), |
| "-s", devicePolicyFile.getAbsolutePath(), |
| deviceSvcFile.getAbsolutePath()); |
| assertTrue("The service_contexts file was invalid:\n" |
| + errorString, errorString.length() == 0); |
| } |
| |
| public static boolean isMac() { |
| String os = System.getProperty("os.name").toLowerCase(); |
| return (os.startsWith("mac") || os.startsWith("darwin")); |
| } |
| |
| private void assertSepolicyTests(String test, String testExecutable, |
| boolean includeVendorSepolicy) throws Exception { |
| sepolicyTests = copyResourceToTempFile(testExecutable); |
| sepolicyTests.setExecutable(true); |
| |
| List<String> args = new ArrayList<String>(); |
| args.add(sepolicyTests.getAbsolutePath()); |
| args.add("-f"); |
| args.add(devicePlatFcFile.getAbsolutePath()); |
| args.add("--test"); |
| args.add(test); |
| |
| if (includeVendorSepolicy) { |
| args.add("-f"); |
| args.add(deviceVendorFcFile.getAbsolutePath()); |
| args.add("-p"); |
| args.add(devicePolicyFile.getAbsolutePath()); |
| } else { |
| args.add("-p"); |
| args.add(deviceSystemPolicyFile.getAbsolutePath()); |
| } |
| |
| String errorString = tryRunCommand(args.toArray(new String[0])); |
| assertTrue(errorString, errorString.length() == 0); |
| |
| sepolicyTests.delete(); |
| } |
| |
| /** |
| * Tests that all types on /data have the data_file_type attribute. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testDataTypeViolators() throws Exception { |
| assertSepolicyTests("TestDataTypeViolations", "/sepolicy_tests", |
| PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */); |
| } |
| |
| /** |
| * Tests that all types in /sys/fs/bpf have the bpffs_type attribute. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testBpffsTypeViolators() throws Exception { |
| assertSepolicyTests("TestBpffsTypeViolations", "/sepolicy_tests", |
| PropertyUtil.isVendorApiLevelNewerThan(mDevice, 33) /* includeVendorSepolicy */); |
| } |
| |
| /** |
| * Tests that all types in /proc have the proc_type attribute. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testProcTypeViolators() throws Exception { |
| assertSepolicyTests("TestProcTypeViolations", "/sepolicy_tests", |
| PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */); |
| } |
| |
| /** |
| * Tests that all types in /sys have the sysfs_type attribute. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testSysfsTypeViolators() throws Exception { |
| assertSepolicyTests("TestSysfsTypeViolations", "/sepolicy_tests", |
| PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */); |
| } |
| |
| /** |
| * Tests that all types on /vendor have the vendor_file_type attribute. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testVendorTypeViolators() throws Exception { |
| assertSepolicyTests("TestVendorTypeViolations", "/sepolicy_tests", |
| PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */); |
| } |
| |
| /** |
| * Tests that tracefs files(/sys/kernel/tracing and /d/tracing) are correctly labeled. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testTracefsTypeViolators() throws Exception { |
| assertSepolicyTests("TestTracefsTypeViolations", "/sepolicy_tests", |
| PropertyUtil.isVendorApiLevelNewerThan(mDevice, 30) /* includeVendorSepolicy */); |
| } |
| |
| /** |
| * Tests that debugfs files(from /sys/kernel/debug) are correctly labeled. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testDebugfsTypeViolators() throws Exception { |
| assertSepolicyTests("TestDebugfsTypeViolations", "/sepolicy_tests", |
| PropertyUtil.isVendorApiLevelNewerThan(mDevice, 30) /* includeVendorSepolicy */); |
| } |
| |
| /** |
| * Tests that all domains with entrypoints on /system have the coredomain |
| * attribute, and that all domains with entrypoints on /vendor do not have the |
| * coredomain attribute. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testCoredomainViolators() throws Exception { |
| assertSepolicyTests("CoredomainViolations", "/sepolicy_tests", |
| PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */); |
| } |
| |
| /** |
| * Tests that all labels on /dev have the dev_type attribute. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testDevTypeViolators() throws Exception { |
| int vsrVersion = getVSRApiLevel(getDevice()); |
| assumeTrue("Skipping test: dev_type is enforced for W or later", vsrVersion > 202404); |
| assertSepolicyTests("TestDevTypeViolations", "/sepolicy_tests", true); |
| } |
| |
| /** |
| * Tests that the policy defines no booleans (runtime conditional policy). |
| * |
| * @throws Exception |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testNoBooleans() throws Exception { |
| |
| /* run sepolicy-analyze booleans check on policy file */ |
| String errorString = tryRunCommand(mSepolicyAnalyze.getAbsolutePath(), |
| devicePolicyFile.getAbsolutePath(), "booleans"); |
| assertTrue("The policy contained booleans:\n" |
| + errorString, errorString.length() == 0); |
| } |
| |
| /** |
| * Tests that taking a bugreport does not produce any dumpstate-related |
| * SELinux denials. |
| * |
| * @throws Exception |
| */ |
| @Test |
| public void testNoBugreportDenials() throws Exception { |
| // Take a bugreport and get its logcat output. |
| mDevice.executeAdbCommand("logcat", "-c"); |
| mDevice.getBugreport(); |
| String log = mDevice.executeAdbCommand("logcat", "-d"); |
| // Find all the dumpstate-related types and make a regex that will match them. |
| Set<String> types = sepolicyAnalyzeGetTypesAssociatedWithAttribute("hal_dumpstate_server"); |
| types.add("dumpstate"); |
| String typeRegex = types.stream().collect(Collectors.joining("|")); |
| Pattern p = Pattern.compile("avc: *denied.*scontext=u:(?:r|object_r):(?:" + typeRegex + "):s0.*"); |
| // Fail if logcat contains such a denial. |
| Matcher m = p.matcher(log); |
| StringBuilder errorString = new StringBuilder(); |
| while (m.find()) { |
| errorString.append(m.group()); |
| errorString.append("\n"); |
| } |
| assertTrue("Found illegal SELinux denial(s): " + errorString, errorString.length() == 0); |
| } |
| |
| /** |
| * Tests that important domain labels are being appropriately applied. |
| */ |
| |
| /** |
| * Asserts that no processes are running in a domain. |
| * |
| * @param domain |
| * The domain or SELinux context to check. |
| */ |
| private void assertDomainEmpty(String domain) throws DeviceNotAvailableException { |
| List<ProcessDetails> procs = ProcessDetails.getProcMap(mDevice).get(domain); |
| String msg = "Expected no processes in SELinux domain \"" + domain + "\"" |
| + " Found: \"" + procs + "\""; |
| assertNull(msg, procs); |
| } |
| |
| /** |
| * Asserts that a domain exists and that only one, well defined, process is |
| * running in that domain. |
| * |
| * @param domain |
| * The domain or SELinux context to check. |
| * @param executable |
| * The path of the executable or application package name. |
| */ |
| private void assertDomainOne(String domain, String executable) throws DeviceNotAvailableException { |
| List<ProcessDetails> procs = ProcessDetails.getProcMap(mDevice).get(domain); |
| List<ProcessDetails> exeProcs = ProcessDetails.getExeMap(mDevice).get(executable); |
| String msg = "Expected 1 process in SELinux domain \"" + domain + "\"" |
| + " Found \"" + procs + "\""; |
| assertNotNull(msg, procs); |
| assertEquals(msg, 1, procs.size()); |
| |
| msg = "Expected executable \"" + executable + "\" in SELinux domain \"" + domain + "\"" |
| + "Found: \"" + procs + "\""; |
| assertEquals(msg, executable, procs.get(0).procTitle); |
| |
| msg = "Expected 1 process with executable \"" + executable + "\"" |
| + " Found \"" + procs + "\""; |
| assertNotNull(msg, exeProcs); |
| assertEquals(msg, 1, exeProcs.size()); |
| |
| msg = "Expected executable \"" + executable + "\" in SELinux domain \"" + domain + "\"" |
| + "Found: \"" + procs + "\""; |
| assertEquals(msg, domain, exeProcs.get(0).label); |
| } |
| |
| /** |
| * Asserts that a domain may exist. If a domain exists, the cardinality of |
| * the domain is verified to be 1 and that the correct process is running in |
| * that domain. If the process is running, it is running in that domain. |
| * |
| * @param domain |
| * The domain or SELinux context to check. |
| * @param executable |
| * The path of the executable or application package name. |
| */ |
| private void assertDomainZeroOrOne(String domain, String executable) |
| throws DeviceNotAvailableException { |
| List<ProcessDetails> procs = ProcessDetails.getProcMap(mDevice).get(domain); |
| List<ProcessDetails> exeProcs = ProcessDetails.getExeMap(mDevice).get(executable); |
| if (procs != null) { |
| String msg = "Expected 1 process in SELinux domain \"" + domain + "\"" |
| + " Found: \"" + procs + "\""; |
| assertEquals(msg, 1, procs.size()); |
| |
| msg = "Expected executable \"" + executable + "\" in SELinux domain \"" + domain + "\"" |
| + "Found: \"" + procs.get(0) + "\""; |
| assertEquals(msg, executable, procs.get(0).procTitle); |
| } |
| if (exeProcs != null) { |
| String msg = "Expected executable \"" + executable + "\" in SELinux domain \"" + domain + "\"" |
| + " Instead found it running in the domain \"" + exeProcs.get(0).label + "\""; |
| assertNotNull(msg, procs); |
| |
| msg = "Expected 1 process with executable \"" + executable + "\"" |
| + " Found: \"" + procs + "\""; |
| assertEquals(msg, 1, exeProcs.size()); |
| |
| msg = "Expected executable \"" + executable + "\" in SELinux domain \"" + domain + "\"" |
| + "Found: \"" + procs.get(0) + "\""; |
| assertEquals(msg, domain, exeProcs.get(0).label); |
| } |
| } |
| |
| /** |
| * Asserts that a domain must exist, and that the cardinality is greater |
| * than or equal to 1. |
| * |
| * @param domain |
| * The domain or SELinux context to check. |
| * @param executables |
| * The path of the allowed executables or application package names. |
| */ |
| private void assertDomainN(String domain, String... executables) |
| throws DeviceNotAvailableException { |
| List<ProcessDetails> procs = ProcessDetails.getProcMap(mDevice).get(domain); |
| String msg = "Expected 1 or more processes in SELinux domain but found none."; |
| assertNotNull(msg, procs); |
| |
| Set<String> execList = new HashSet<String>(Arrays.asList(executables)); |
| |
| for (ProcessDetails p : procs) { |
| msg = "Expected one of \"" + execList + "\" in SELinux domain \"" + domain + "\"" |
| + " Found: \"" + p + "\""; |
| assertTrue(msg, execList.contains(p.procTitle)); |
| } |
| |
| for (String exe : executables) { |
| List<ProcessDetails> exeProcs = ProcessDetails.getExeMap(mDevice).get(exe); |
| |
| if (exeProcs != null) { |
| for (ProcessDetails p : exeProcs) { |
| msg = "Expected executable \"" + exe + "\" in SELinux domain \"" |
| + domain + "\"" + " Found: \"" + p + "\""; |
| assertEquals(msg, domain, p.label); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Asserts that a domain, if it exists, is only running the listed executables. |
| * |
| * @param domain |
| * The domain or SELinux context to check. |
| * @param executables |
| * The path of the allowed executables or application package names. |
| */ |
| private void assertDomainHasExecutable(String domain, String... executables) |
| throws DeviceNotAvailableException { |
| List<ProcessDetails> procs = ProcessDetails.getProcMap(mDevice).get(domain); |
| |
| if (procs != null) { |
| Set<String> execList = new HashSet<String>(Arrays.asList(executables)); |
| |
| for (ProcessDetails p : procs) { |
| String msg = "Expected one of \"" + execList + "\" in SELinux domain \"" |
| + domain + "\"" + " Found: \"" + p + "\""; |
| assertTrue(msg, execList.contains(p.procTitle)); |
| } |
| } |
| } |
| |
| /** |
| * Asserts that an executable exists and is only running in the listed domains. |
| * |
| * @param executable |
| * The path of the executable to check. |
| * @param domains |
| * The list of allowed domains. |
| */ |
| private void assertExecutableExistsAndHasDomain(String executable, String... domains) |
| throws DeviceNotAvailableException { |
| List<ProcessDetails> exeProcs = ProcessDetails.getExeMap(mDevice).get(executable); |
| Set<String> domainList = new HashSet<String>(Arrays.asList(domains)); |
| |
| String msg = "Expected 1 or more processes for executable \"" + executable + "\"."; |
| assertNotNull(msg, exeProcs); |
| |
| for (ProcessDetails p : exeProcs) { |
| msg = "Expected one of \"" + domainList + "\" for executable \"" + executable |
| + "\"" + " Found: \"" + p.label + "\""; |
| assertTrue(msg, domainList.contains(p.label)); |
| } |
| } |
| |
| /** |
| * Asserts that an executable, if it exists, is only running in the listed domains. |
| * |
| * @param executable |
| * The path of the executable to check. |
| * @param domains |
| * The list of allowed domains. |
| */ |
| private void assertExecutableHasDomain(String executable, String... domains) |
| throws DeviceNotAvailableException { |
| List<ProcessDetails> exeProcs = ProcessDetails.getExeMap(mDevice).get(executable); |
| Set<String> domainList = new HashSet<String>(Arrays.asList(domains)); |
| |
| if (exeProcs != null) { |
| for (ProcessDetails p : exeProcs) { |
| String msg = "Expected one of \"" + domainList + "\" for executable \"" + executable |
| + "\"" + " Found: \"" + p.label + "\""; |
| assertTrue(msg, domainList.contains(p.label)); |
| } |
| } |
| } |
| |
| /* Init is always there */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testInitDomain() throws DeviceNotAvailableException { |
| assertDomainHasExecutable("u:r:init:s0", "/system/bin/init"); |
| assertDomainHasExecutable("u:r:vendor_init:s0", "/system/bin/init"); |
| assertExecutableExistsAndHasDomain("/system/bin/init", "u:r:init:s0", "u:r:vendor_init:s0"); |
| } |
| |
| /* Ueventd is always there */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testUeventdDomain() throws DeviceNotAvailableException { |
| assertDomainOne("u:r:ueventd:s0", "/system/bin/ueventd"); |
| } |
| |
| /* healthd may or may not exist */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testHealthdDomain() throws DeviceNotAvailableException { |
| assertDomainZeroOrOne("u:r:healthd:s0", "/system/bin/healthd"); |
| } |
| |
| /* Servicemanager is always there */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testServicemanagerDomain() throws DeviceNotAvailableException { |
| assertDomainOne("u:r:servicemanager:s0", "/system/bin/servicemanager"); |
| } |
| |
| /* Vold is always there */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testVoldDomain() throws DeviceNotAvailableException { |
| assertDomainOne("u:r:vold:s0", "/system/bin/vold"); |
| } |
| |
| /* netd is always there */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testNetdDomain() throws DeviceNotAvailableException { |
| assertDomainN("u:r:netd:s0", "/system/bin/netd", "/system/bin/iptables-restore", "/system/bin/ip6tables-restore"); |
| } |
| |
| /* Surface flinger is always there */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testSurfaceflingerDomain() throws DeviceNotAvailableException { |
| assertDomainOne("u:r:surfaceflinger:s0", "/system/bin/surfaceflinger"); |
| } |
| |
| /* Zygote is always running */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testZygoteDomain() throws DeviceNotAvailableException { |
| assertDomainN("u:r:zygote:s0", "zygote", "zygote64", "usap32", "usap64"); |
| } |
| |
| /* Checks drmserver for devices that require it */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testDrmServerDomain() throws DeviceNotAvailableException { |
| assertDomainHasExecutable("u:r:drmserver:s0", "/system/bin/drmserver", "/system/bin/drmserver32", "/system/bin/drmserver64"); |
| } |
| |
| /* Installd is always running */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testInstalldDomain() throws DeviceNotAvailableException { |
| assertDomainOne("u:r:installd:s0", "/system/bin/installd"); |
| } |
| |
| /* keystore is always running */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testKeystoreDomain() throws DeviceNotAvailableException { |
| assertDomainOne("u:r:keystore:s0", "/system/bin/keystore2"); |
| } |
| |
| /* System server better be running :-P */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testSystemServerDomain() throws DeviceNotAvailableException { |
| assertDomainOne("u:r:system_server:s0", "system_server"); |
| } |
| |
| /* Watchdogd may or may not be there */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testWatchdogdDomain() throws DeviceNotAvailableException { |
| assertDomainZeroOrOne("u:r:watchdogd:s0", "/system/bin/watchdogd"); |
| } |
| |
| /* logd may or may not be there */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testLogdDomain() throws DeviceNotAvailableException { |
| assertDomainZeroOrOne("u:r:logd:s0", "/system/bin/logd"); |
| } |
| |
| /* lmkd may or may not be there */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testLmkdDomain() throws DeviceNotAvailableException { |
| assertDomainZeroOrOne("u:r:lmkd:s0", "/system/bin/lmkd"); |
| } |
| |
| /* Wifi may be off so cardinality of 0 or 1 is ok */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testWpaDomain() throws DeviceNotAvailableException { |
| assertDomainZeroOrOne("u:r:wpa:s0", "/system/bin/wpa_supplicant"); |
| } |
| |
| /* permissioncontroller, if running, always runs in permissioncontroller_app */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testPermissionControllerDomain() throws DeviceNotAvailableException { |
| assertExecutableHasDomain("com.google.android.permissioncontroller", "u:r:permissioncontroller_app:s0"); |
| assertExecutableHasDomain("com.android.permissioncontroller", "u:r:permissioncontroller_app:s0"); |
| } |
| |
| /* vzwomatrigger may or may not be running */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testVzwOmaTriggerDomain() throws DeviceNotAvailableException { |
| assertDomainZeroOrOne("u:r:vzwomatrigger_app:s0", "com.android.vzwomatrigger"); |
| } |
| |
| /* gmscore, if running, always runs in gmscore_app */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testGMSCoreDomain() throws DeviceNotAvailableException { |
| assertExecutableHasDomain("com.google.android.gms", "u:r:gmscore_app:s0"); |
| assertExecutableHasDomain("com.google.android.gms.ui", "u:r:gmscore_app:s0"); |
| assertExecutableHasDomain("com.google.android.gms.persistent", "u:r:gmscore_app:s0"); |
| assertExecutableHasDomain("com.google.android.gms:snet", "u:r:gmscore_app:s0"); |
| } |
| |
| /* |
| * Nothing should be running in this domain, cardinality test is all thats |
| * needed |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testInitShellDomain() throws DeviceNotAvailableException { |
| assertDomainEmpty("u:r:init_shell:s0"); |
| } |
| |
| /* |
| * Nothing should be running in this domain, cardinality test is all thats |
| * needed |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testRecoveryDomain() throws DeviceNotAvailableException { |
| assertDomainEmpty("u:r:recovery:s0"); |
| } |
| |
| /* |
| * Nothing should be running in this domain, cardinality test is all thats |
| * needed |
| */ |
| @CddTest(requirement="9.7") |
| @RestrictedBuildTest |
| @Test |
| public void testSuDomain() throws DeviceNotAvailableException { |
| assertDomainEmpty("u:r:su:s0"); |
| } |
| |
| /* |
| * All kthreads should be in kernel context. |
| */ |
| @CddTest(requirement="9.7") |
| @Test |
| public void testKernelDomain() throws DeviceNotAvailableException { |
| String domain = "u:r:kernel:s0"; |
| List<ProcessDetails> procs = ProcessDetails.getProcMap(mDevice).get(domain); |
| if (procs != null) { |
| for (ProcessDetails p : procs) { |
| assertTrue("Non Kernel thread \"" + p + "\" found!", p.isKernel()); |
| } |
| } |
| } |
| |
| private static class ProcessDetails { |
| public String label; |
| public String user; |
| public int pid; |
| public int ppid; |
| public String procTitle; |
| |
| private static HashMap<String, ArrayList<ProcessDetails>> procMap; |
| private static HashMap<String, ArrayList<ProcessDetails>> exeMap; |
| private static int kernelParentThreadpid = -1; |
| |
| ProcessDetails(String label, String user, int pid, int ppid, String procTitle) { |
| this.label = label; |
| this.user = user; |
| this.pid = pid; |
| this.ppid = ppid; |
| this.procTitle = procTitle; |
| } |
| |
| @Override |
| public String toString() { |
| return "label: " + label |
| + " user: " + user |
| + " pid: " + pid |
| + " ppid: " + ppid |
| + " cmd: " + procTitle; |
| } |
| |
| |
| private static void createProcMap(ITestDevice tDevice) throws DeviceNotAvailableException { |
| |
| /* take the output of a ps -Z to do our analysis */ |
| CollectingOutputReceiver psOut = new CollectingOutputReceiver(); |
| // TODO: remove "toybox" below and just run "ps" |
| tDevice.executeShellCommand("toybox ps -A -o label,user,pid,ppid,cmdline", psOut); |
| String psOutString = psOut.getOutput(); |
| Pattern p = Pattern.compile( |
| "^([\\w_:,]+)\\s+([\\w_]+)\\s+(\\d+)\\s+(\\d+)\\s+(\\p{Graph}+)(\\s\\p{Graph}+)*\\s*$" |
| ); |
| procMap = new HashMap<String, ArrayList<ProcessDetails>>(); |
| exeMap = new HashMap<String, ArrayList<ProcessDetails>>(); |
| for(String line : psOutString.split("\n")) { |
| Matcher m = p.matcher(line); |
| if(m.matches()) { |
| String domainLabel = m.group(1); |
| // clean up the domainlabel |
| String[] parts = domainLabel.split(":"); |
| if (parts.length > 4) { |
| // we have an extra categories bit at the end consisting of cxxx,cxxx ... |
| // just make the domain out of the first 4 parts |
| domainLabel = String.join(":", parts[0], parts[1], parts[2], parts[3]); |
| } |
| |
| String user = m.group(2); |
| int pid = Integer.parseInt(m.group(3)); |
| int ppid = Integer.parseInt(m.group(4)); |
| String procTitle = m.group(5); |
| ProcessDetails proc = new ProcessDetails(domainLabel, user, pid, ppid, procTitle); |
| if (procMap.get(domainLabel) == null) { |
| procMap.put(domainLabel, new ArrayList<ProcessDetails>()); |
| } |
| procMap.get(domainLabel).add(proc); |
| if (procTitle.equals("[kthreadd]") && ppid == 0) { |
| kernelParentThreadpid = pid; |
| } |
| if (exeMap.get(procTitle) == null) { |
| exeMap.put(procTitle, new ArrayList<ProcessDetails>()); |
| } |
| exeMap.get(procTitle).add(proc); |
| } |
| } |
| } |
| |
| public static HashMap<String, ArrayList<ProcessDetails>> getProcMap(ITestDevice tDevice) |
| throws DeviceNotAvailableException{ |
| if (procMap == null) { |
| createProcMap(tDevice); |
| } |
| return procMap; |
| } |
| |
| public static HashMap<String, ArrayList<ProcessDetails>> getExeMap(ITestDevice tDevice) |
| throws DeviceNotAvailableException{ |
| if (exeMap == null) { |
| createProcMap(tDevice); |
| } |
| return exeMap; |
| } |
| |
| public boolean isKernel() { |
| return (pid == kernelParentThreadpid || ppid == kernelParentThreadpid); |
| } |
| } |
| |
| private static String tryRunCommand(String... command) throws Exception { |
| ProcessBuilder pb = new ProcessBuilder(command); |
| pb.redirectOutput(ProcessBuilder.Redirect.PIPE); |
| pb.redirectErrorStream(true); |
| Process p = pb.start(); |
| p.waitFor(); |
| BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); |
| StringBuilder result = new StringBuilder(); |
| String line; |
| while ((line = reader.readLine()) != null) { |
| result.append(line); |
| result.append("\n"); |
| } |
| return result.toString(); |
| } |
| |
| private static File createTempFile(String name, String ext) throws IOException { |
| File ret = File.createTempFile(name, ext); |
| ret.deleteOnExit(); |
| return ret; |
| } |
| } |