• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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