1#!/usr/bin/env python3 2# 3# Copyright (C) 2020 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18import logging 19import os 20import shutil 21import tempfile 22import unittest 23 24from importlib import resources 25 26from vts.testcases.vndk import utils 27from vts.utils.python.android import api 28 29PERMISSION_GROUPS = 3 # 3 permission groups: owner, group, all users 30READ_PERMISSION = 4 31WRITE_PERMISSION = 2 32EXECUTE_PERMISSION = 1 33 34def HasPermission(permission_bits, groupIndex, permission): 35 """Determines if the permission bits grant a permission to a group. 36 37 Args: 38 permission_bits: string, the octal permissions string (e.g. 741) 39 groupIndex: int, the index of the group into the permissions string. 40 (e.g. 0 is owner group). If set to -1, then all groups are 41 checked. 42 permission: the value of the permission. 43 44 Returns: 45 True if the group(s) has read permission. 46 47 Raises: 48 ValueError if the group or permission bits are invalid 49 """ 50 if groupIndex >= PERMISSION_GROUPS: 51 raise ValueError("Invalid group: %s" % str(groupIndex)) 52 53 if len(permission_bits) != PERMISSION_GROUPS: 54 raise ValueError("Invalid permission bits: %s" % str(permission_bits)) 55 56 # Define the start/end group index 57 start = groupIndex 58 end = groupIndex + 1 59 if groupIndex < 0: 60 start = 0 61 end = PERMISSION_GROUPS 62 63 for i in range(start, end): 64 perm = int(permission_bits[i]) # throws ValueError if not an integer 65 if perm > 7: 66 raise ValueError("Invalid permission bit: %s" % str(perm)) 67 if perm & permission == 0: 68 # Return false if any group lacks the permission 69 return False 70 # Return true if no group lacks the permission 71 return True 72 73 74def IsReadable(permission_bits): 75 """Determines if the permission bits grant read permission to any group. 76 77 Args: 78 permission_bits: string, the octal permissions string (e.g. 741) 79 80 Returns: 81 True if any group has read permission. 82 83 Raises: 84 ValueError if the group or permission bits are invalid 85 """ 86 return any([ 87 HasPermission(permission_bits, i, READ_PERMISSION) 88 for i in range(PERMISSION_GROUPS) 89 ]) 90 91class VtsTrebleSysPropTest(unittest.TestCase): 92 """Test case which check compatibility of system property. 93 94 Attributes: 95 _temp_dir: The temporary directory to which necessary files are copied. 96 _PUBLIC_PROPERTY_CONTEXTS_FILE_PATH: The path of public property 97 contexts file. 98 _SYSTEM_PROPERTY_CONTEXTS_FILE_PATH: The path of system property 99 contexts file. 100 _PRODUCT_PROPERTY_CONTEXTS_FILE_PATH: The path of product property 101 contexts file. 102 _VENDOR_PROPERTY_CONTEXTS_FILE_PATH: The path of vendor property 103 contexts file. 104 _ODM_PROPERTY_CONTEXTS_FILE_PATH: The path of odm property 105 contexts file. 106 _VENDOR_OR_ODM_NAMESPACES: The namespaces allowed for vendor/odm 107 properties. 108 _VENDOR_OR_ODM_NAMESPACES_WHITELIST: The extra namespaces allowed for 109 vendor/odm properties. 110 _VENDOR_TYPE_PREFIX: Expected prefix for the vendor prop types 111 _ODM_TYPE_PREFIX: Expected prefix for the odm prop types 112 _SYSTEM_WHITELISTED_TYPES: System props are not allowed to start with 113 "vendor_", but these are exceptions. 114 _VENDOR_OR_ODM_WHITELISTED_TYPES: vendor/odm props must start with 115 "vendor_" or "odm_", but these are exceptions. 116 """ 117 118 _PUBLIC_PROPERTY_CONTEXTS_FILE_PATH = ("public/property_contexts") 119 _SYSTEM_PROPERTY_CONTEXTS_FILE_PATH = ("/system/etc/selinux/" 120 "plat_property_contexts") 121 _PRODUCT_PROPERTY_CONTEXTS_FILE_PATH = ("/product/etc/selinux/" 122 "product_property_contexts") 123 _VENDOR_PROPERTY_CONTEXTS_FILE_PATH = ("/vendor/etc/selinux/" 124 "vendor_property_contexts") 125 _ODM_PROPERTY_CONTEXTS_FILE_PATH = ("/odm/etc/selinux/" 126 "odm_property_contexts") 127 _VENDOR_OR_ODM_NAMESPACES = [ 128 "ctl.odm.", 129 "ctl.vendor.", 130 "ctl.start$odm.", 131 "ctl.start$vendor.", 132 "ctl.stop$odm.", 133 "ctl.stop$vendor.", 134 "init.svc.odm.", 135 "init.svc.vendor.", 136 "ro.boot.", 137 "ro.hardware.", 138 "ro.odm.", 139 "ro.vendor.", 140 "odm.", 141 "persist.odm.", 142 "persist.vendor.", 143 "vendor." 144 ] 145 146 _VENDOR_OR_ODM_NAMESPACES_WHITELIST = [ 147 "persist.camera." # b/138545066 remove this 148 ] 149 150 _VENDOR_TYPE_PREFIX = "vendor_" 151 152 _ODM_TYPE_PREFIX = "odm_" 153 154 _SYSTEM_WHITELISTED_TYPES = [ 155 "vendor_default_prop", 156 "vendor_security_patch_level_prop", 157 "vendor_socket_hook_prop" 158 ] 159 160 _VENDOR_OR_ODM_WHITELISTED_TYPES = [ 161 ] 162 163 def setUp(self): 164 """Initializes tests. 165 166 Data file path, device, remote shell instance and temporary directory 167 are initialized. 168 """ 169 serial_number = os.environ.get("ANDROID_SERIAL") 170 self.assertTrue(serial_number, "$ANDROID_SERIAL is empty.") 171 self.dut = utils.AndroidDevice(serial_number) 172 self._temp_dir = tempfile.mkdtemp() 173 174 def tearDown(self): 175 """Deletes the temporary directory.""" 176 logging.info("Delete %s", self._temp_dir) 177 shutil.rmtree(self._temp_dir) 178 179 def _ParsePropertyDictFromPropertyContextsFile(self, 180 property_contexts_file, 181 exact_only=False): 182 """Parse property contexts file to a dictionary. 183 184 Args: 185 property_contexts_file: file object of property contexts file 186 exact_only: whether parsing only properties which require exact 187 matching 188 189 Returns: 190 dict: {property_name: property_tokens} where property_tokens[1] 191 is selinux type of the property, e.g. u:object_r:my_prop:s0 192 """ 193 property_dict = dict() 194 for line in property_contexts_file.readlines(): 195 tokens = line.strip().rstrip("\n").split() 196 if len(tokens) > 0 and not tokens[0].startswith("#"): 197 if not exact_only: 198 property_dict[tokens[0]] = tokens 199 elif len(tokens) >= 4 and tokens[2] == "exact": 200 property_dict[tokens[0]] = tokens 201 202 return property_dict 203 204 def testActionableCompatiblePropertyEnabled(self): 205 """Ensures the feature of actionable compatible property is enforced. 206 207 ro.actionable_compatible_property.enabled must be true to enforce the 208 feature of actionable compatible property. 209 """ 210 self.assertEqual( 211 self.dut._GetProp("ro.actionable_compatible_property.enabled"), 212 "true", "ro.actionable_compatible_property.enabled must be true") 213 214 def _TestVendorOrOdmPropertyNames(self, partition, contexts_path): 215 logging.info("Checking existence of %s", contexts_path) 216 self.AssertPermissionsAndExistence( 217 contexts_path, IsReadable) 218 219 # Pull property contexts file from device. 220 self.dut.AdbPull(contexts_path, self._temp_dir) 221 logging.info("Adb pull %s to %s", contexts_path, self._temp_dir) 222 223 with open( 224 os.path.join(self._temp_dir, 225 "%s_property_contexts" % partition), 226 "r") as property_contexts_file: 227 property_dict = self._ParsePropertyDictFromPropertyContextsFile( 228 property_contexts_file) 229 logging.info("Found %d property names in %s property contexts", 230 len(property_dict), partition) 231 violation_list = list(filter( 232 lambda x: not any( 233 x.startswith(prefix) for prefix in 234 self._VENDOR_OR_ODM_NAMESPACES + 235 self._VENDOR_OR_ODM_NAMESPACES_WHITELIST), 236 property_dict.keys())) 237 self.assertEqual( 238 # Transfer filter to list for python3. 239 len(violation_list), 0, 240 ("%s properties (%s) have wrong namespace" % 241 (partition, " ".join(sorted(violation_list))))) 242 243 def _TestPropertyTypes(self, property_contexts_file, check_function): 244 fd, downloaded = tempfile.mkstemp(dir=self._temp_dir) 245 os.close(fd) 246 self.dut.AdbPull(property_contexts_file, downloaded) 247 logging.info("adb pull %s to %s", property_contexts_file, downloaded) 248 249 with open(downloaded, "r") as f: 250 property_dict = self._ParsePropertyDictFromPropertyContextsFile(f) 251 logging.info("Found %d properties from %s", 252 len(property_dict), property_contexts_file) 253 254 # Filter props that don't satisfy check_function. 255 # tokens[1] is something like u:object_r:my_prop:s0 256 violation_list = [(name, tokens) for name, tokens in 257 property_dict.items() 258 if not check_function(tokens[1].split(":")[2])] 259 260 self.assertEqual( 261 len(violation_list), 0, 262 "properties in %s have wrong property types:\n%s" % ( 263 property_contexts_file, 264 "\n".join("name: %s, type: %s" % (name, tokens[1]) 265 for name, tokens in violation_list)) 266 ) 267 268 def testVendorPropertyNames(self): 269 """Ensures vendor properties have proper namespace. 270 271 Vendor or ODM properties must have their own prefix. 272 """ 273 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_P: 274 logging.info("Skip test for a device which launched first before " 275 "Android Q.") 276 return 277 self._TestVendorOrOdmPropertyNames( 278 "vendor", self._VENDOR_PROPERTY_CONTEXTS_FILE_PATH) 279 280 281 def testOdmPropertyNames(self): 282 """Ensures odm properties have proper namespace. 283 284 Vendor or ODM properties must have their own prefix. 285 """ 286 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_P: 287 logging.info("Skip test for a device which launched first before " 288 "Android Q.") 289 return 290 if (not self.dut.Exists(self._ODM_PROPERTY_CONTEXTS_FILE_PATH)): 291 logging.info("Skip test for a device which doesn't have an odm " 292 "property contexts.") 293 return 294 self._TestVendorOrOdmPropertyNames( 295 "odm", self._ODM_PROPERTY_CONTEXTS_FILE_PATH) 296 297 def testProductPropertyNames(self): 298 """Ensures product properties have proper namespace. 299 300 Product properties must not have Vendor or ODM namespaces. 301 """ 302 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_P: 303 logging.info("Skip test for a device which launched first before " 304 "Android Q.") 305 return 306 if (not self.dut.Exists(self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH)): 307 logging.info("Skip test for a device which doesn't have an product " 308 "property contexts.") 309 return 310 311 logging.info("Checking existence of %s", 312 self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH) 313 self.AssertPermissionsAndExistence( 314 self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH, 315 IsReadable) 316 317 # Pull product property contexts file from device. 318 self.dut.AdbPull(self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH, 319 self._temp_dir) 320 logging.info("Adb pull %s to %s", 321 self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH, self._temp_dir) 322 323 with open(os.path.join(self._temp_dir, "product_property_contexts"), 324 "r") as property_contexts_file: 325 property_dict = self._ParsePropertyDictFromPropertyContextsFile( 326 property_contexts_file, True) 327 logging.info( 328 "Found %d property names in product property contexts", 329 len(property_dict)) 330 331 violation_list = list(filter( 332 lambda x: any( 333 x.startswith(prefix) 334 for prefix in self._VENDOR_OR_ODM_NAMESPACES), 335 property_dict.keys())) 336 self.assertEqual( 337 len(violation_list), 0, 338 ("product propertes (%s) have wrong namespace" % 339 " ".join(sorted(violation_list)))) 340 341 def testPlatformPropertyTypes(self): 342 """Ensures properties in the system partition have valid types""" 343 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_Q: 344 logging.info("Skip test for a device which launched first before " 345 "Android Q.") 346 return 347 self._TestPropertyTypes( 348 self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH, 349 lambda typename: ( 350 not typename.startswith(self._VENDOR_TYPE_PREFIX) and 351 not typename.startswith(self._ODM_TYPE_PREFIX) and 352 typename not in self._VENDOR_OR_ODM_WHITELISTED_TYPES 353 ) or typename in self._SYSTEM_WHITELISTED_TYPES) 354 355 def testVendorPropertyTypes(self): 356 """Ensures properties in the vendor partion have valid types""" 357 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_Q: 358 logging.info("Skip test for a device which launched first before " 359 "Android Q.") 360 return 361 self._TestPropertyTypes( 362 self._VENDOR_PROPERTY_CONTEXTS_FILE_PATH, 363 lambda typename: typename.startswith(self._VENDOR_TYPE_PREFIX) or 364 typename in self._VENDOR_OR_ODM_WHITELISTED_TYPES) 365 366 def testOdmPropertyTypes(self): 367 """Ensures properties in the odm partition have valid types""" 368 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_Q: 369 logging.info("Skip test for a device which launched first before " 370 "Android Q.") 371 return 372 if (not self.dut.Exists(self._ODM_PROPERTY_CONTEXTS_FILE_PATH)): 373 logging.info("Skip test for a device which doesn't have an odm " 374 "property contexts.") 375 return 376 self._TestPropertyTypes( 377 self._ODM_PROPERTY_CONTEXTS_FILE_PATH, 378 lambda typename: typename.startswith(self._VENDOR_TYPE_PREFIX) or 379 typename.startswith(self._ODM_TYPEPREFIX) or 380 typename in self._VENDOR_OR_ODM_WHITELISTED_TYPES) 381 382 def testExportedPlatformPropertyIntegrity(self): 383 """Ensures public property contexts isn't modified at all. 384 385 Public property contexts must not be modified. 386 """ 387 logging.info("Checking existence of %s", 388 self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH) 389 self.AssertPermissionsAndExistence( 390 self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH, 391 IsReadable) 392 393 # Pull system property contexts file from device. 394 self.dut.AdbPull(self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH, 395 self._temp_dir) 396 logging.info("Adb pull %s to %s", 397 self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH, self._temp_dir) 398 399 with open(os.path.join(self._temp_dir, "plat_property_contexts"), 400 "r") as property_contexts_file: 401 sys_property_dict = self._ParsePropertyDictFromPropertyContextsFile( 402 property_contexts_file, True) 403 logging.info( 404 "Found %d exact-matching properties " 405 "in system property contexts", len(sys_property_dict)) 406 407 # Extract data from parfile. 408 resource_name = os.path.basename(self._PUBLIC_PROPERTY_CONTEXTS_FILE_PATH) 409 package_name = os.path.dirname( 410 self._PUBLIC_PROPERTY_CONTEXTS_FILE_PATH).replace(os.path.sep, '.') 411 with resources.open_text(package_name, resource_name) as resource: 412 pub_property_dict = self._ParsePropertyDictFromPropertyContextsFile( 413 resource, True) 414 for name in pub_property_dict: 415 public_tokens = pub_property_dict[name] 416 self.assertTrue(name in sys_property_dict, 417 "Exported property (%s) doesn't exist" % name) 418 system_tokens = sys_property_dict[name] 419 self.assertEqual(public_tokens, system_tokens, 420 "Exported property (%s) is modified" % name) 421 422 423 def AssertPermissionsAndExistence(self, path, check_permission): 424 """Asserts that the specified path exists and has the correct permission. 425 Args: 426 path: string, path to validate existence and permissions 427 check_permission: function which takes unix permissions in octalformat 428 and returns True if the permissions are correct, 429 False otherwise. 430 """ 431 self.assertTrue(self.dut.Exists(path), "%s: File does not exist." % path) 432 try: 433 permission = self.GetPermission(path) 434 self.assertTrue(check_permission(permission), 435 "%s: File has invalid permissions (%s)" % (path, permission)) 436 except (ValueError, IOError) as e: 437 assertIsNone(e, "Failed to assert permissions: %s" % str(e)) 438 439 def GetPermission(self, path): 440 """Read the file permission bits of a path. 441 442 Args: 443 filepath: string, path to a file or directory 444 445 Returns: 446 String, octal permission bits for the path 447 448 Raises: 449 IOError if the path does not exist or has invalid permission bits. 450 """ 451 cmd = "stat -c %%a %s" % path 452 out, err, return_code = self.dut.Execute(cmd) 453 logging.debug("%s: Shell command '%s' out: %s, err: %s, return_code: %s", path, cmd, out, err, return_code) 454 # checks the exit code 455 if return_code != 0: 456 raise IOError(err) 457 accessBits = out.strip() 458 if len(accessBits) != 3: 459 raise IOError("%s: Wrong number of access bits (%s)" % (path, accessBits)) 460 return accessBits 461 462if __name__ == "__main__": 463 # Setting verbosity is required to generate output that the TradeFed test 464 # runner can parse. 465 unittest.main(verbosity=3) 466