• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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 // Suppress "SecurityManager is deprecated" warnings: FileReadSandboxTest verifies that the class
17 // still works as expected
18 @file:Suppress("DEPRECATION")
19 
20 package com.android.tools.metalava
21 
22 import java.io.File
23 import java.io.FileDescriptor
24 import java.net.InetAddress
25 import java.security.Permission
26 import kotlin.concurrent.getOrSet
27 
28 /**
29  * Detect access to files that not explicitly specified in the command line.
30  *
31  * This class detects reads on both files and directories. Directory accesses are logged by
32  * the driver in [run], which only logs it, but doesn't consider it an error.
33  *
34  * We do not prevent reads on directories that are not explicitly listed in the command line because
35  * metalava (or JVM, really) seems to read some system directories such as /usr/, etc., but this
36  * behavior may be JVM dependent so we do not want to have to explicitly include them.
37  * (Because, otherwise, when we update JVM, it may access different directories and we end up
38  * having to update the implicit allowed list.) As long as we don't read files, reading directories
39  * shouldn't (normally) affect the result, so we simply allow any directory reads.
40  */
41 internal object FileReadSandbox {
42     private var installed = false
43 
44     private var previousSecurityManager: SecurityManager? = null
45     private val mySecurityManager = MySecurityManager()
46 
47     /**
48      * Set of paths that are allowed to access. This is used with an exact match.
49      */
50     private var allowedPaths = mutableSetOf<String>()
51 
52     /**
53      * Set of path prefixes that are allowed to access.
54      */
55     private var allowedPathPrefixes = mutableSetOf<String>()
56 
57     interface Listener {
58         /** Called when a violation is detected with the absolute path. */
59         fun onViolation(absolutePath: String, isDirectory: Boolean)
60     }
61 
62     private var listener: Listener? = null
63 
64     init {
65         initialize()
66     }
67 
68     private fun initialize() {
69         allowedPaths.clear()
70         allowedPathPrefixes.clear()
71         listener = null
72 
73         // Allow certain directories by default.
74         // The sandbox detects all file accessed done by JVM implicitly too (e.g. JVM seems to
75         // access /dev/urandom on linux), so we need to allow them.
76         // At least on linux, there doesn't seem to be any access to files under /proc/ or /sys/,
77         // but they should be allowed anyway.
78         listOf("/dev", "/proc", "/sys").forEach {
79             allowAccess(File(it))
80         }
81         // Allow access to the directory containing the metalava's jar itself.
82         // (so even if metalava loads other jar files in the same directory, that wouldn't be a
83         // violation.)
84         FileReadSandbox::class.java.protectionDomain?.codeSource?.location?.toURI()?.path?.let {
85             allowAccess(File(it).parentFile)
86         }
87 
88         // Allow access to $JAVA_HOME.
89         // We also allow $ANDROID_JAVA_HOME, which is used in the Android platform build.
90         // (which is normally $ANDROID_BUILD_TOP + "/prebuilts/jdk/jdk11/linux-x86" as of writing.)
91         listOf(
92             "JAVA_HOME",
93             "ANDROID_JAVA_HOME"
94         ).forEach {
95             System.getenv(it)?.let { path ->
96                 allowAccess(File(path))
97             }
98         }
99         // JVM seems to use ~/.cache/
100         System.getenv("HOME")?.let {
101             allowAccess(File("$it/.cache"))
102         }
103     }
104 
105     /** Activate the sandbox. */
106     fun activate(listener: Listener) {
107         if (installed) {
108             throw IllegalStateException("Already activated")
109         }
110         previousSecurityManager = System.getSecurityManager()
111         System.setSecurityManager(mySecurityManager)
112         installed = true
113         this.listener = listener
114     }
115 
116     /** Deactivate the sandbox. */
117     fun deactivate() {
118         if (!installed) {
119             throw IllegalStateException("Not activated")
120         }
121         System.setSecurityManager(previousSecurityManager)
122         previousSecurityManager = null
123 
124         installed = false
125     }
126 
127     /** Reset all the state and re-initialize the sandbox. Only callable when not activated. */
128     fun reset() {
129         if (installed) {
130             throw IllegalStateException("Can't reset. Already activated")
131         }
132         initialize()
133     }
134 
135     /** Allows access to files or directories. */
136     fun allowAccess(files: List<File>): List<File> {
137         files.forEach { allowAccess(it) }
138         return files
139     }
140 
141     /** Allows access to a file or directory. */
142     fun <T : File?> allowAccess(file: T): T {
143         if (file == null) {
144             return file
145         }
146         val path = file.absolutePath
147         val added = allowedPaths.add(path)
148         if (file.isDirectory) {
149             // Even if it had already been added to allowedPaths (i.e. added == false),
150             // the directory might have just been created, so we still do it.
151             allowedPathPrefixes.add("$path/")
152         }
153         if (!added) {
154             // Already added; bail early.
155             return file
156         }
157 
158         // Allow all parent directories. But don't allow prefix accesses (== access to the
159         // directory itself is okay, but don't grant access to any files/directories under it).
160         var parent = file.parentFile
161         while (true) {
162             if (parent == null) {
163                 break
164             }
165             @Suppress("NAME_SHADOWING")
166             val path = parent.absolutePath
167             if (!allowedPaths.add(path)) {
168                 break // already added.
169             }
170             if (path == "/") {
171                 break
172             }
173             parent = parent.parentFile
174         }
175         return file
176     }
177 
178     fun isAccessAllowed(file: File): Boolean {
179         if (!file.exists()) {
180             return true
181         }
182         val absPath = file.absolutePath
183 
184         if (allowedPaths.contains(absPath)) {
185             return true
186         }
187         allowedPathPrefixes.forEach {
188             if (absPath.startsWith(it)) {
189                 return true
190             }
191         }
192         return false
193     }
194 
195     fun isDirectory(file: File): Boolean {
196         try {
197             temporaryExempt.set(true)
198             return file.isDirectory()
199         } finally {
200             temporaryExempt.set(false)
201         }
202     }
203 
204     fun isFile(file: File): Boolean {
205         try {
206             temporaryExempt.set(true)
207             return file.isFile()
208         } finally {
209             temporaryExempt.set(false)
210         }
211     }
212 
213     /** Used to skip all checks on any filesystem access made within the [check] method. */
214     private val temporaryExempt = ThreadLocal<Boolean>()
215 
216     private fun check(path: String) {
217         if (temporaryExempt.getOrSet { false }) {
218             return
219         }
220 
221         try {
222             temporaryExempt.set(true)
223 
224             val file = File(path)
225 
226             if (!isAccessAllowed(file)) {
227                 listener?.onViolation(file.absolutePath, file.isDirectory)
228             }
229         } finally {
230             temporaryExempt.set(false)
231         }
232     }
233 
234     /**
235      * Reading files that are created by metalava should be allowed, so we detect file writes to
236      * new files, and add them to the allowed path list.
237      */
238     private fun writeDetected(origPath: String?) {
239         origPath ?: return
240 
241         if (temporaryExempt.getOrSet { false }) {
242             return
243         }
244 
245         try {
246             temporaryExempt.set(true)
247 
248             val file = File(origPath)
249             if (file.exists()) {
250                 return // Ignore writes to an existing file / directory.
251             }
252             allowedPaths.add(file.absolutePath)
253         } finally {
254             temporaryExempt.set(false)
255         }
256     }
257 
258     private class MySecurityManager : SecurityManager() {
259         override fun checkRead(file: String) {
260             check(file)
261         }
262 
263         override fun checkRead(file: String, context: Any?) {
264             check(file)
265         }
266         override fun checkRead(p0: FileDescriptor?) {
267         }
268 
269         override fun checkDelete(p0: String?) {
270         }
271 
272         override fun checkPropertiesAccess() {
273         }
274 
275         override fun checkAccess(p0: Thread?) {
276         }
277 
278         override fun checkAccess(p0: ThreadGroup?) {
279         }
280 
281         override fun checkExec(p0: String?) {
282         }
283 
284         override fun checkListen(p0: Int) {
285         }
286 
287         override fun checkExit(p0: Int) {
288         }
289 
290         override fun checkLink(p0: String?) {
291         }
292 
293         override fun checkPropertyAccess(p0: String?) {
294         }
295 
296         override fun checkPackageDefinition(p0: String?) {
297         }
298 
299         override fun checkMulticast(p0: InetAddress?) {
300         }
301 
302         override fun checkMulticast(p0: InetAddress?, p1: Byte) {
303         }
304 
305         override fun checkPermission(p0: Permission?) {
306         }
307 
308         override fun checkPermission(p0: Permission?, p1: Any?) {
309         }
310 
311         override fun checkPackageAccess(p0: String?) {
312         }
313 
314         override fun checkAccept(p0: String?, p1: Int) {
315         }
316 
317         override fun checkSecurityAccess(p0: String?) {
318         }
319 
320         override fun checkWrite(p0: FileDescriptor?) {
321         }
322 
323         override fun checkWrite(file: String?) {
324             writeDetected(file)
325         }
326 
327         override fun checkPrintJobAccess() {
328         }
329 
330         override fun checkCreateClassLoader() {
331         }
332 
333         override fun checkConnect(p0: String?, p1: Int) {
334         }
335 
336         override fun checkConnect(p0: String?, p1: Int, p2: Any?) {
337         }
338 
339         override fun checkSetFactory() {
340         }
341     }
342 }
343