• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 
15 package com.google.common.io;
16 
17 import static com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR;
18 import static com.google.common.base.StandardSystemProperty.USER_NAME;
19 import static com.google.common.base.Throwables.throwIfUnchecked;
20 import static java.nio.file.attribute.AclEntryFlag.DIRECTORY_INHERIT;
21 import static java.nio.file.attribute.AclEntryFlag.FILE_INHERIT;
22 import static java.nio.file.attribute.AclEntryType.ALLOW;
23 import static java.nio.file.attribute.PosixFilePermissions.asFileAttribute;
24 import static java.util.Objects.requireNonNull;
25 
26 import com.google.common.annotations.GwtIncompatible;
27 import com.google.common.annotations.J2ktIncompatible;
28 import com.google.common.annotations.VisibleForTesting;
29 import com.google.common.collect.ImmutableList;
30 import com.google.j2objc.annotations.J2ObjCIncompatible;
31 import java.io.File;
32 import java.io.IOException;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.nio.file.FileSystems;
36 import java.nio.file.Paths;
37 import java.nio.file.attribute.AclEntry;
38 import java.nio.file.attribute.AclEntryPermission;
39 import java.nio.file.attribute.FileAttribute;
40 import java.nio.file.attribute.PosixFilePermissions;
41 import java.nio.file.attribute.UserPrincipal;
42 import java.util.EnumSet;
43 import java.util.Set;
44 
45 /**
46  * Creates temporary files and directories whose permissions are restricted to the current user or,
47  * in the case of Android, the current app. If that is not possible (as is the case under the very
48  * old Android Ice Cream Sandwich release), then this class throws an exception instead of creating
49  * a file or directory that would be more accessible.
50  */
51 @J2ktIncompatible
52 @GwtIncompatible
53 @J2ObjCIncompatible
54 @ElementTypesAreNonnullByDefault
55 abstract class TempFileCreator {
56   static final TempFileCreator INSTANCE = pickSecureCreator();
57 
58   /**
59    * @throws IllegalStateException if the directory could not be created (to implement the contract
60    *     of {@link Files#createTempDir()}, such as if the system does not support creating temporary
61    *     directories securely
62    */
createTempDir()63   abstract File createTempDir();
64 
createTempFile(String prefix)65   abstract File createTempFile(String prefix) throws IOException;
66 
pickSecureCreator()67   private static TempFileCreator pickSecureCreator() {
68     try {
69       Class.forName("java.nio.file.Path");
70       return new JavaNioCreator();
71     } catch (ClassNotFoundException runningUnderAndroid) {
72       // Try another way.
73     }
74 
75     try {
76       int version = (int) Class.forName("android.os.Build$VERSION").getField("SDK_INT").get(null);
77       int jellyBean =
78           (int) Class.forName("android.os.Build$VERSION_CODES").getField("JELLY_BEAN").get(null);
79       /*
80        * I assume that this check can't fail because JELLY_BEAN will be present only if we're
81        * running under Jelly Bean or higher. But it seems safest to check.
82        */
83       if (version < jellyBean) {
84         return new ThrowingCreator();
85       }
86 
87       // Don't merge these catch() blocks, let alone use ReflectiveOperationException directly:
88       // b/65343391
89     } catch (NoSuchFieldException e) {
90       // The JELLY_BEAN field doesn't exist because we're running on a version before Jelly Bean :)
91       return new ThrowingCreator();
92     } catch (ClassNotFoundException e) {
93       // Should be impossible, but we want to return *something* so that class init succeeds.
94       return new ThrowingCreator();
95     } catch (IllegalAccessException e) {
96       // ditto
97       return new ThrowingCreator();
98     }
99 
100     // Android isolates apps' temporary directories since Jelly Bean:
101     // https://github.com/google/guava/issues/4011#issuecomment-770020802
102     // So we can create files there with any permissions and still get security from the isolation.
103     return new JavaIoCreator();
104   }
105 
106   /**
107    * Creates the permissions normally used for Windows filesystems, looking up the user afresh, even
108    * if previous calls have initialized the {@code PermissionSupplier} fields.
109    *
110    * <p>This lets us test the effects of different values of the {@code user.name} system property
111    * without needing a separate VM or classloader.
112    */
113   @IgnoreJRERequirement // used only when Path is available (and only from tests)
114   @VisibleForTesting
testMakingUserPermissionsFromScratch()115   static void testMakingUserPermissionsFromScratch() throws IOException {
116     // All we're testing is whether it throws.
117     FileAttribute<?> unused = JavaNioCreator.userPermissions().get();
118   }
119 
120   @IgnoreJRERequirement // used only when Path is available
121   private static final class JavaNioCreator extends TempFileCreator {
122     @Override
createTempDir()123     File createTempDir() {
124       try {
125         return java.nio.file.Files.createTempDirectory(
126                 Paths.get(JAVA_IO_TMPDIR.value()), /* prefix= */ null, directoryPermissions.get())
127             .toFile();
128       } catch (IOException e) {
129         throw new IllegalStateException("Failed to create directory", e);
130       }
131     }
132 
133     @Override
createTempFile(String prefix)134     File createTempFile(String prefix) throws IOException {
135       return java.nio.file.Files.createTempFile(
136               Paths.get(JAVA_IO_TMPDIR.value()),
137               /* prefix= */ prefix,
138               /* suffix= */ null,
139               filePermissions.get())
140           .toFile();
141     }
142 
143     @IgnoreJRERequirement // see enclosing class (whose annotation Animal Sniffer ignores here...)
144     private interface PermissionSupplier {
get()145       FileAttribute<?> get() throws IOException;
146     }
147 
148     private static final PermissionSupplier filePermissions;
149     private static final PermissionSupplier directoryPermissions;
150 
151     static {
152       Set<String> views = FileSystems.getDefault().supportedFileAttributeViews();
153       if (views.contains("posix")) {
154         filePermissions = () -> asFileAttribute(PosixFilePermissions.fromString("rw-------"));
155         directoryPermissions = () -> asFileAttribute(PosixFilePermissions.fromString("rwx------"));
156       } else if (views.contains("acl")) {
157         filePermissions = directoryPermissions = userPermissions();
158       } else {
159         filePermissions =
160             directoryPermissions =
161                 () -> {
162                   throw new IOException("unrecognized FileSystem type " + FileSystems.getDefault());
163                 };
164       }
165     }
166 
userPermissions()167     private static PermissionSupplier userPermissions() {
168       try {
169         UserPrincipal user =
170             FileSystems.getDefault()
171                 .getUserPrincipalLookupService()
172                 .lookupPrincipalByName(getUsername());
173         ImmutableList<AclEntry> acl =
174             ImmutableList.of(
175                 AclEntry.newBuilder()
176                     .setType(ALLOW)
177                     .setPrincipal(user)
178                     .setPermissions(EnumSet.allOf(AclEntryPermission.class))
179                     .setFlags(DIRECTORY_INHERIT, FILE_INHERIT)
180                     .build());
181         FileAttribute<ImmutableList<AclEntry>> attribute =
182             new FileAttribute<ImmutableList<AclEntry>>() {
183               @Override
184               public String name() {
185                 return "acl:acl";
186               }
187 
188               @Override
189               public ImmutableList<AclEntry> value() {
190                 return acl;
191               }
192             };
193         return () -> attribute;
194       } catch (IOException e) {
195         // We throw a new exception each time so that the stack trace is right.
196         return () -> {
197           throw new IOException("Could not find user", e);
198         };
199       }
200     }
201 
getUsername()202     private static String getUsername() {
203       /*
204        * https://github.com/google/guava/issues/6634: ProcessHandle has more accurate information,
205        * but that class isn't available under all environments that we support. We use it if
206        * available and fall back if not.
207        */
208       String fromSystemProperty = requireNonNull(USER_NAME.value());
209 
210       try {
211         Class<?> processHandleClass = Class.forName("java.lang.ProcessHandle");
212         Class<?> processHandleInfoClass = Class.forName("java.lang.ProcessHandle$Info");
213         Class<?> optionalClass = Class.forName("java.util.Optional");
214         /*
215          * We don't *need* to use reflection to access Optional: It's available on all JDKs we
216          * support, and Android code won't get this far, anyway, because ProcessHandle is
217          * unavailable. But given how much other reflection we're using, we might as well use it
218          * here, too, so that we don't need to also suppress an AndroidApiChecker error.
219          */
220 
221         Method currentMethod = processHandleClass.getMethod("current");
222         Method infoMethod = processHandleClass.getMethod("info");
223         Method userMethod = processHandleInfoClass.getMethod("user");
224         Method orElseMethod = optionalClass.getMethod("orElse", Object.class);
225 
226         Object current = currentMethod.invoke(null);
227         Object info = infoMethod.invoke(current);
228         Object user = userMethod.invoke(info);
229         return (String) requireNonNull(orElseMethod.invoke(user, fromSystemProperty));
230       } catch (ClassNotFoundException runningUnderAndroidOrJava8) {
231         /*
232          * I'm not sure that we could actually get here for *Android*: I would expect us to enter
233          * the POSIX code path instead. And if we tried this code path, we'd have trouble unless we
234          * were running under a new enough version of Android to support NIO.
235          *
236          * So this is probably just the "Windows Java 8" case. In that case, if we wanted *another*
237          * layer of fallback before consulting the system property, we could try
238          * com.sun.security.auth.module.NTSystem.
239          *
240          * But for now, we use the value from the system property as our best guess.
241          */
242         return fromSystemProperty;
243       } catch (InvocationTargetException e) {
244         throwIfUnchecked(e.getCause()); // in case it's an Error or something
245         return fromSystemProperty; // should be impossible
246       } catch (NoSuchMethodException shouldBeImpossible) {
247         return fromSystemProperty;
248       } catch (IllegalAccessException shouldBeImpossible) {
249         /*
250          * We don't merge these into `catch (ReflectiveOperationException ...)` or an equivalent
251          * multicatch because ReflectiveOperationException isn't available under Android:
252          * b/124188803
253          */
254         return fromSystemProperty;
255       }
256     }
257   }
258 
259   private static final class JavaIoCreator extends TempFileCreator {
260     @Override
createTempDir()261     File createTempDir() {
262       File baseDir = new File(JAVA_IO_TMPDIR.value());
263       @SuppressWarnings("GoodTime") // reading system time without TimeSource
264       String baseName = System.currentTimeMillis() + "-";
265 
266       for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
267         File tempDir = new File(baseDir, baseName + counter);
268         if (tempDir.mkdir()) {
269           return tempDir;
270         }
271       }
272       throw new IllegalStateException(
273           "Failed to create directory within "
274               + TEMP_DIR_ATTEMPTS
275               + " attempts (tried "
276               + baseName
277               + "0 to "
278               + baseName
279               + (TEMP_DIR_ATTEMPTS - 1)
280               + ')');
281     }
282 
283     @Override
createTempFile(String prefix)284     File createTempFile(String prefix) throws IOException {
285       return File.createTempFile(
286           /* prefix= */ prefix,
287           /* suffix= */ null,
288           /* directory= */ null /* defaults to java.io.tmpdir */);
289     }
290 
291     /** Maximum loop count when creating temp directories. */
292     private static final int TEMP_DIR_ATTEMPTS = 10000;
293   }
294 
295   private static final class ThrowingCreator extends TempFileCreator {
296     private static final String MESSAGE =
297         "Guava cannot securely create temporary files or directories under SDK versions before"
298             + " Jelly Bean. You can create one yourself, either in the insecure default directory"
299             + " or in a more secure directory, such as context.getCacheDir(). For more information,"
300             + " see the Javadoc for Files.createTempDir().";
301 
302     @Override
createTempDir()303     File createTempDir() {
304       throw new IllegalStateException(MESSAGE);
305     }
306 
307     @Override
createTempFile(String prefix)308     File createTempFile(String prefix) throws IOException {
309       throw new IOException(MESSAGE);
310     }
311   }
312 
TempFileCreator()313   private TempFileCreator() {}
314 }
315