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