1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.tools.metalava 17 18 import java.io.File 19 import java.io.FileDescriptor 20 import java.net.InetAddress 21 import java.security.Permission 22 import kotlin.concurrent.getOrSet 23 24 /** 25 * Detect access to files that not explicitly specified in the command line. 26 * 27 * This class detects reads on both files and directories. Directory accesses are logged by 28 * [Driver], which only logs it, but doesn't consider it an error. 29 * 30 * We do not prevent reads on directories that are not explicitly listed in the command line because 31 * metalava (or JVM, really) seems to read some system directories such as /usr/, etc., but this 32 * behavior may be JVM dependent so we do not want to have to explicitly whitelist them. 33 * (Because, otherwise, when we update JVM, it may access different directories and we end up 34 * having to update the implicit whitelist.) As long as we don't read files, reading directories 35 * shouldn't (normally) affect the result, so we simply allow any directory reads. 36 */ 37 internal object FileReadSandbox { 38 private var installed = false 39 40 private var previousSecurityManager: SecurityManager? = null 41 private val mySecurityManager = MySecurityManager() 42 43 /** 44 * Set of paths that are allowed to access. This is used with an exact match. 45 */ 46 private var allowedPaths = mutableSetOf<String>() 47 48 /** 49 * Set of path prefixes that are allowed to access. 50 */ 51 private var allowedPathPrefixes = mutableSetOf<String>() 52 53 interface Listener { 54 /** Called when a violation is detected with the absolute path. */ onViolationnull55 fun onViolation(absolutePath: String, isDirectory: Boolean) 56 } 57 58 private var listener: Listener? = null 59 60 init { 61 initialize() 62 } 63 initializenull64 private fun initialize() { 65 allowedPaths.clear() 66 allowedPathPrefixes.clear() 67 listener = null 68 69 // Allow certain directories by default. 70 // The sandbox detects all file accessed done by JVM implicitly too (e.g. JVM seems to 71 // access /dev/urandom on linux), so we need to allow them. 72 // At least on linux, there doesn't seem to be any access to files under /proc/ or /sys/, 73 // but they should be allowed anyway. 74 listOf("/dev", "/proc", "/sys").forEach { 75 allowAccess(File(it)) 76 } 77 // Allow access to the directory containing the metalava's jar itself. 78 // (so even if metalava loads other jar files in the same directory, that wouldn't be a 79 // violation.) 80 FileReadSandbox::class.java.protectionDomain?.codeSource?.location?.toURI()?.path?.let { 81 allowAccess(File(it).parentFile) 82 } 83 84 // Allow access to $JAVA_HOME. 85 // We also allow $ANDROID_JAVA_HOME, which is used in the Android platform build. 86 // (which is normally $ANDROID_BUILD_TOP + "/prebuilts/jdk/jdk11/linux-x86" as of writing.) 87 listOf( 88 "JAVA_HOME", 89 "ANDROID_JAVA_HOME" 90 ).forEach { 91 System.getenv(it)?.let { 92 allowAccess(File(it)) 93 } 94 } 95 // JVM seems to use ~/.cache/ 96 System.getenv("HOME")?.let { 97 allowAccess(File("$it/.cache")) 98 } 99 } 100 101 /** Activate the sandbox. */ activatenull102 fun activate(listener: Listener) { 103 if (installed) { 104 throw IllegalStateException("Already activated") 105 } 106 previousSecurityManager = System.getSecurityManager() 107 System.setSecurityManager(mySecurityManager) 108 installed = true 109 this.listener = listener 110 } 111 112 /** Deactivate the sandbox. This also resets [violationCount]. */ deactivatenull113 fun deactivate() { 114 if (!installed) { 115 throw IllegalStateException("Not activated") 116 } 117 System.setSecurityManager(previousSecurityManager) 118 previousSecurityManager = null 119 120 installed = false 121 } 122 123 /** Reset all the state and re-initialize the sandbox. Only callable when not activated. */ resetnull124 fun reset() { 125 if (installed) { 126 throw IllegalStateException("Can't reset. Already activated") 127 } 128 initialize() 129 } 130 131 /** Allows access to files or directories. */ allowAccessnull132 fun allowAccess(files: List<File>): List<File> { 133 files.forEach { allowAccess(it) } 134 return files 135 } 136 137 /** Allows access to a file or directory. */ allowAccessnull138 fun <T : File?> allowAccess(file: T): T { 139 if (file == null) { 140 return file 141 } 142 val path = file.absolutePath 143 val added = allowedPaths.add(path) 144 if (file.isDirectory) { 145 // Even if it had already been added to allowedPaths (i.e. added == false), 146 // the directory might have just been created, so we still do it. 147 allowedPathPrefixes.add("$path/") 148 } 149 if (!added) { 150 // Already added; bail early. 151 return file 152 } 153 154 // Whitelist all parent directories. But don't allow prefix accesses (== access to the 155 // directory itself is okay, but don't grant access to any files/directories under it). 156 var parent = file.parentFile 157 while (true) { 158 if (parent == null) { 159 break 160 } 161 @Suppress("NAME_SHADOWING") 162 val path = parent.absolutePath 163 if (!allowedPaths.add(path)) { 164 break // already added. 165 } 166 if (path == "/") { 167 break 168 } 169 parent = parent.parentFile 170 } 171 return file 172 } 173 isAccessAllowednull174 fun isAccessAllowed(file: File): Boolean { 175 if (!file.exists()) { 176 return true 177 } 178 val absPath = file.absolutePath 179 180 if (allowedPaths.contains(absPath)) { 181 return true 182 } 183 allowedPathPrefixes.forEach { 184 if (absPath.startsWith(it)) { 185 return true 186 } 187 } 188 return false 189 } 190 isDirectorynull191 fun isDirectory(file: File): Boolean { 192 try { 193 temporaryExempt.set(true) 194 return file.isDirectory() 195 } finally { 196 temporaryExempt.set(false) 197 } 198 } 199 isFilenull200 fun isFile(file: File): Boolean { 201 try { 202 temporaryExempt.set(true) 203 return file.isFile() 204 } finally { 205 temporaryExempt.set(false) 206 } 207 } 208 209 /** Used to skip all checks on any filesystem access made within the [check] method. */ 210 private val temporaryExempt = ThreadLocal<Boolean>() 211 checknull212 private fun check(path: String) { 213 if (temporaryExempt.getOrSet { false }) { 214 return 215 } 216 217 try { 218 temporaryExempt.set(true) 219 220 val file = File(path) 221 222 if (!isAccessAllowed(file)) { 223 listener?.onViolation(file.absolutePath, file.isDirectory) 224 } 225 } finally { 226 temporaryExempt.set(false) 227 } 228 } 229 230 /** 231 * Reading files that are created by metalava should be allowed, so we detect file writes to 232 * new files, and whitelist it. 233 */ writeDetectednull234 private fun writeDetected(origPath: String?) { 235 if (temporaryExempt.getOrSet { false }) { 236 return 237 } 238 239 try { 240 temporaryExempt.set(true) 241 242 val file = File(origPath) 243 if (file.exists()) { 244 return // Ignore writes to an existing file / directory. 245 } 246 allowedPaths.add(file.absolutePath) 247 } finally { 248 temporaryExempt.set(false) 249 } 250 } 251 252 private class MySecurityManager : SecurityManager() { checkReadnull253 override fun checkRead(file: String) { 254 check(file) 255 } 256 checkReadnull257 override fun checkRead(file: String, context: Any?) { 258 check(file) 259 } checkReadnull260 override fun checkRead(p0: FileDescriptor?) { 261 } 262 checkDeletenull263 override fun checkDelete(p0: String?) { 264 } 265 checkPropertiesAccessnull266 override fun checkPropertiesAccess() { 267 } 268 checkAccessnull269 override fun checkAccess(p0: Thread?) { 270 } 271 checkAccessnull272 override fun checkAccess(p0: ThreadGroup?) { 273 } 274 checkExecnull275 override fun checkExec(p0: String?) { 276 } 277 checkListennull278 override fun checkListen(p0: Int) { 279 } 280 checkExitnull281 override fun checkExit(p0: Int) { 282 } 283 checkLinknull284 override fun checkLink(p0: String?) { 285 } 286 checkPropertyAccessnull287 override fun checkPropertyAccess(p0: String?) { 288 } 289 checkPackageDefinitionnull290 override fun checkPackageDefinition(p0: String?) { 291 } 292 checkMulticastnull293 override fun checkMulticast(p0: InetAddress?) { 294 } 295 checkMulticastnull296 override fun checkMulticast(p0: InetAddress?, p1: Byte) { 297 } 298 checkPermissionnull299 override fun checkPermission(p0: Permission?) { 300 } 301 checkPermissionnull302 override fun checkPermission(p0: Permission?, p1: Any?) { 303 } 304 checkPackageAccessnull305 override fun checkPackageAccess(p0: String?) { 306 } 307 checkAcceptnull308 override fun checkAccept(p0: String?, p1: Int) { 309 } 310 checkSecurityAccessnull311 override fun checkSecurityAccess(p0: String?) { 312 } 313 checkWritenull314 override fun checkWrite(p0: FileDescriptor?) { 315 } 316 checkWritenull317 override fun checkWrite(file: String?) { 318 writeDetected(file) 319 } 320 checkPrintJobAccessnull321 override fun checkPrintJobAccess() { 322 } 323 checkCreateClassLoadernull324 override fun checkCreateClassLoader() { 325 } 326 checkConnectnull327 override fun checkConnect(p0: String?, p1: Int) { 328 } 329 checkConnectnull330 override fun checkConnect(p0: String?, p1: Int, p2: Any?) { 331 } 332 checkSetFactorynull333 override fun checkSetFactory() { 334 } 335 } 336 } 337