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 = ("private/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 # This exception is allowed only for the devices launched before S 147 _VENDOR_OR_ODM_NAMESPACES_WHITELIST = [ 148 "persist.camera.", 149 "persist.dumpstate.verbose_logging.enabled", 150 ] 151 152 _VENDOR_TYPE_PREFIX = "vendor_" 153 154 _ODM_TYPE_PREFIX = "odm_" 155 156 _SYSTEM_WHITELISTED_TYPES = [ 157 "vendor_default_prop", 158 "vendor_security_patch_level_prop", 159 "vendor_socket_hook_prop" 160 ] 161 162 _VENDOR_OR_ODM_WHITELISTED_TYPES = [ 163 ] 164 165 def setUp(self): 166 """Initializes tests. 167 168 Data file path, device, remote shell instance and temporary directory 169 are initialized. 170 """ 171 serial_number = os.environ.get("ANDROID_SERIAL") 172 self.assertTrue(serial_number, "$ANDROID_SERIAL is empty.") 173 self.dut = utils.AndroidDevice(serial_number) 174 self._temp_dir = tempfile.mkdtemp() 175 176 def tearDown(self): 177 """Deletes the temporary directory.""" 178 logging.info("Delete %s", self._temp_dir) 179 shutil.rmtree(self._temp_dir) 180 181 def _ParsePropertyDictFromPropertyContextsFile(self, 182 property_contexts_file, 183 exact_only=False): 184 """Parse property contexts file to a dictionary. 185 186 Args: 187 property_contexts_file: file object of property contexts file 188 exact_only: whether parsing only properties which require exact 189 matching 190 191 Returns: 192 dict: {property_name: property_tokens} where property_tokens[1] 193 is selinux type of the property, e.g. u:object_r:my_prop:s0 194 """ 195 property_dict = dict() 196 for line in property_contexts_file.readlines(): 197 tokens = line.strip().rstrip("\n").split() 198 if len(tokens) > 0 and not tokens[0].startswith("#"): 199 if not exact_only: 200 property_dict[tokens[0]] = tokens 201 elif len(tokens) >= 4 and tokens[2] == "exact": 202 property_dict[tokens[0]] = tokens 203 204 return property_dict 205 206 def testActionableCompatiblePropertyEnabled(self): 207 """Ensures the feature of actionable compatible property is enforced. 208 209 ro.actionable_compatible_property.enabled must be true to enforce the 210 feature of actionable compatible property. 211 """ 212 self.assertEqual( 213 self.dut._GetProp("ro.actionable_compatible_property.enabled"), 214 "true", "ro.actionable_compatible_property.enabled must be true") 215 216 def _TestVendorOrOdmPropertyNames(self, partition, contexts_path): 217 logging.info("Checking existence of %s", contexts_path) 218 self.AssertPermissionsAndExistence( 219 contexts_path, IsReadable) 220 221 # Pull property contexts file from device. 222 self.dut.AdbPull(contexts_path, self._temp_dir) 223 logging.info("Adb pull %s to %s", contexts_path, self._temp_dir) 224 225 with open( 226 os.path.join(self._temp_dir, 227 "%s_property_contexts" % partition), 228 "r") as property_contexts_file: 229 property_dict = self._ParsePropertyDictFromPropertyContextsFile( 230 property_contexts_file) 231 logging.info("Found %d property names in %s property contexts", 232 len(property_dict), partition) 233 234 allowed_namespaces = self._VENDOR_OR_ODM_NAMESPACES.copy() 235 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_R: 236 allowed_namespaces += self._VENDOR_OR_ODM_NAMESPACES_WHITELIST 237 238 violation_list = list(filter( 239 lambda x: not any( 240 x.startswith(prefix) for prefix in allowed_namespaces), 241 property_dict.keys())) 242 self.assertEqual( 243 # Transfer filter to list for python3. 244 len(violation_list), 0, 245 ("%s properties (%s) have wrong namespace" % 246 (partition, " ".join(sorted(violation_list))))) 247 248 def _TestPropertyTypes(self, property_contexts_file, check_function): 249 fd, downloaded = tempfile.mkstemp(dir=self._temp_dir) 250 os.close(fd) 251 self.dut.AdbPull(property_contexts_file, downloaded) 252 logging.info("adb pull %s to %s", property_contexts_file, downloaded) 253 254 with open(downloaded, "r") as f: 255 property_dict = self._ParsePropertyDictFromPropertyContextsFile(f) 256 logging.info("Found %d properties from %s", 257 len(property_dict), property_contexts_file) 258 259 # Filter props that don't satisfy check_function. 260 # tokens[1] is something like u:object_r:my_prop:s0 261 violation_list = [(name, tokens) for name, tokens in 262 property_dict.items() 263 if not check_function(tokens[1].split(":")[2])] 264 265 self.assertEqual( 266 len(violation_list), 0, 267 "properties in %s have wrong property types:\n%s" % ( 268 property_contexts_file, 269 "\n".join("name: %s, type: %s" % (name, tokens[1]) 270 for name, tokens in violation_list)) 271 ) 272 273 def testVendorPropertyNames(self): 274 """Ensures vendor properties have proper namespace. 275 276 Vendor or ODM properties must have their own prefix. 277 """ 278 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_P: 279 logging.info("Skip test for a device which launched first before " 280 "Android Q.") 281 return 282 self._TestVendorOrOdmPropertyNames( 283 "vendor", self._VENDOR_PROPERTY_CONTEXTS_FILE_PATH) 284 285 286 def testOdmPropertyNames(self): 287 """Ensures odm properties have proper namespace. 288 289 Vendor or ODM properties must have their own prefix. 290 """ 291 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_P: 292 logging.info("Skip test for a device which launched first before " 293 "Android Q.") 294 return 295 if (not self.dut.Exists(self._ODM_PROPERTY_CONTEXTS_FILE_PATH)): 296 logging.info("Skip test for a device which doesn't have an odm " 297 "property contexts.") 298 return 299 self._TestVendorOrOdmPropertyNames( 300 "odm", self._ODM_PROPERTY_CONTEXTS_FILE_PATH) 301 302 def testProductPropertyNames(self): 303 """Ensures product properties have proper namespace. 304 305 Product properties must not have Vendor or ODM namespaces. 306 """ 307 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_P: 308 logging.info("Skip test for a device which launched first before " 309 "Android Q.") 310 return 311 if (not self.dut.Exists(self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH)): 312 logging.info("Skip test for a device which doesn't have an product " 313 "property contexts.") 314 return 315 316 logging.info("Checking existence of %s", 317 self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH) 318 self.AssertPermissionsAndExistence( 319 self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH, 320 IsReadable) 321 322 # Pull product property contexts file from device. 323 self.dut.AdbPull(self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH, 324 self._temp_dir) 325 logging.info("Adb pull %s to %s", 326 self._PRODUCT_PROPERTY_CONTEXTS_FILE_PATH, self._temp_dir) 327 328 with open(os.path.join(self._temp_dir, "product_property_contexts"), 329 "r") as property_contexts_file: 330 property_dict = self._ParsePropertyDictFromPropertyContextsFile( 331 property_contexts_file, True) 332 logging.info( 333 "Found %d property names in product property contexts", 334 len(property_dict)) 335 336 violation_list = list(filter( 337 lambda x: any( 338 x.startswith(prefix) 339 for prefix in self._VENDOR_OR_ODM_NAMESPACES), 340 property_dict.keys())) 341 self.assertEqual( 342 len(violation_list), 0, 343 ("product propertes (%s) have wrong namespace" % 344 " ".join(sorted(violation_list)))) 345 346 def testPlatformPropertyTypes(self): 347 """Ensures properties in the system partition have valid types""" 348 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_Q: 349 logging.info("Skip test for a device which launched first before " 350 "Android Q.") 351 return 352 self._TestPropertyTypes( 353 self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH, 354 lambda typename: ( 355 not typename.startswith(self._VENDOR_TYPE_PREFIX) and 356 not typename.startswith(self._ODM_TYPE_PREFIX) and 357 typename not in self._VENDOR_OR_ODM_WHITELISTED_TYPES 358 ) or typename in self._SYSTEM_WHITELISTED_TYPES) 359 360 def testVendorPropertyTypes(self): 361 """Ensures properties in the vendor partion have valid types""" 362 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_Q: 363 logging.info("Skip test for a device which launched first before " 364 "Android Q.") 365 return 366 self._TestPropertyTypes( 367 self._VENDOR_PROPERTY_CONTEXTS_FILE_PATH, 368 lambda typename: typename.startswith(self._VENDOR_TYPE_PREFIX) or 369 typename in self._VENDOR_OR_ODM_WHITELISTED_TYPES) 370 371 def testOdmPropertyTypes(self): 372 """Ensures properties in the odm partition have valid types""" 373 if self.dut.GetLaunchApiLevel() <= api.PLATFORM_API_LEVEL_Q: 374 logging.info("Skip test for a device which launched first before " 375 "Android Q.") 376 return 377 if (not self.dut.Exists(self._ODM_PROPERTY_CONTEXTS_FILE_PATH)): 378 logging.info("Skip test for a device which doesn't have an odm " 379 "property contexts.") 380 return 381 self._TestPropertyTypes( 382 self._ODM_PROPERTY_CONTEXTS_FILE_PATH, 383 lambda typename: typename.startswith(self._VENDOR_TYPE_PREFIX) or 384 typename.startswith(self._ODM_TYPE_PREFIX) or 385 typename in self._VENDOR_OR_ODM_WHITELISTED_TYPES) 386 387 def testExportedPlatformPropertyIntegrity(self): 388 """Ensures public property contexts isn't modified at all. 389 390 Public property contexts must not be modified. 391 """ 392 logging.info("Checking existence of %s", 393 self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH) 394 self.AssertPermissionsAndExistence( 395 self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH, 396 IsReadable) 397 398 # Pull system property contexts file from device. 399 self.dut.AdbPull(self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH, 400 self._temp_dir) 401 logging.info("Adb pull %s to %s", 402 self._SYSTEM_PROPERTY_CONTEXTS_FILE_PATH, self._temp_dir) 403 404 with open(os.path.join(self._temp_dir, "plat_property_contexts"), 405 "r") as property_contexts_file: 406 sys_property_dict = self._ParsePropertyDictFromPropertyContextsFile( 407 property_contexts_file, True) 408 logging.info( 409 "Found %d exact-matching properties " 410 "in system property contexts", len(sys_property_dict)) 411 412 # Extract data from parfile. 413 resource_name = os.path.basename(self._PUBLIC_PROPERTY_CONTEXTS_FILE_PATH) 414 package_name = os.path.dirname( 415 self._PUBLIC_PROPERTY_CONTEXTS_FILE_PATH).replace(os.path.sep, '.') 416 with resources.files(package_name).joinpath(resource_name).open('r') \ 417 as resource: 418 pub_property_dict = self._ParsePropertyDictFromPropertyContextsFile( 419 resource, True) 420 for name in pub_property_dict: 421 public_tokens = pub_property_dict[name] 422 self.assertTrue(name in sys_property_dict, 423 "Exported property (%s) doesn't exist" % name) 424 system_tokens = sys_property_dict[name] 425 self.assertEqual(public_tokens, system_tokens, 426 "Exported property (%s) is modified" % name) 427 428 429 def AssertPermissionsAndExistence(self, path, check_permission): 430 """Asserts that the specified path exists and has the correct permission. 431 Args: 432 path: string, path to validate existence and permissions 433 check_permission: function which takes unix permissions in octalformat 434 and returns True if the permissions are correct, 435 False otherwise. 436 """ 437 self.assertTrue(self.dut.Exists(path), "%s: File does not exist." % path) 438 try: 439 permission = self.GetPermission(path) 440 self.assertTrue(check_permission(permission), 441 "%s: File has invalid permissions (%s)" % (path, permission)) 442 except (ValueError, IOError) as e: 443 assertIsNone(e, "Failed to assert permissions: %s" % str(e)) 444 445 def GetPermission(self, path): 446 """Read the file permission bits of a path. 447 448 Args: 449 filepath: string, path to a file or directory 450 451 Returns: 452 String, octal permission bits for the path 453 454 Raises: 455 IOError if the path does not exist or has invalid permission bits. 456 """ 457 cmd = ["stat", "-c", "%a", path] 458 out, err, return_code = self.dut.Execute(*cmd) 459 logging.debug("%s: Shell command '%s' out: %s, err: %s, return_code: %s", path, cmd, out, err, return_code) 460 # checks the exit code 461 if return_code != 0: 462 raise IOError(err) 463 accessBits = out.strip() 464 if len(accessBits) != 3: 465 raise IOError("%s: Wrong number of access bits (%s)" % (path, accessBits)) 466 return accessBits 467 468if __name__ == "__main__": 469 # Setting verbosity is required to generate output that the TradeFed test 470 # runner can parse. 471 unittest.main(verbosity=3) 472