1 /*
2  * Copyright (C) 2009 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 android.os.cts;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 
20 import android.content.Context;
21 import android.content.pm.ApplicationInfo;
22 import android.os.Environment;
23 import android.os.UserHandle;
24 import android.platform.test.annotations.AppModeFull;
25 import android.platform.test.annotations.AppModeSdkSandbox;
26 import android.system.Os;
27 import android.system.StructStatVfs;
28 
29 import androidx.test.platform.app.InstrumentationRegistry;
30 
31 import com.android.compatibility.common.util.ApiTest;
32 
33 import junit.framework.TestCase;
34 
35 import java.io.BufferedReader;
36 import java.io.File;
37 import java.io.FileReader;
38 import java.util.UUID;
39 import java.util.function.BiFunction;
40 import java.util.function.Function;
41 
42 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
43 public class EnvironmentTest extends TestCase {
44 
45     private static final Context sContext =
46             InstrumentationRegistry.getInstrumentation().getContext();
47 
testEnvironment()48     public void testEnvironment() {
49         new Environment();
50         assertNotNull(Environment.getExternalStorageState());
51         assertTrue(Environment.getRootDirectory().isDirectory());
52         assertTrue(Environment.getDownloadCacheDirectory().isDirectory());
53         assertTrue(Environment.getDataDirectory().isDirectory());
54     }
55 
56     @AppModeFull(reason = "External directory not accessible by instant apps")
testEnvironmentExternal()57     public void testEnvironmentExternal() {
58         assertTrue(Environment.getStorageDirectory().isDirectory());
59         assertTrue(Environment.getExternalStorageDirectory().isDirectory());
60     }
61 
62     /**
63      * If TMPDIR points to a global shared directory,
64      * this could compromise the security of the files.
65      */
testNoTmpDir()66     public void testNoTmpDir() {
67         assertTrue(System.getenv("TMPDIR").endsWith("android.os.cts/cache"));
68     }
69 
70     /**
71      * Verify that all writable block filesystems are mounted "noatime" to avoid
72      * unnecessary flash churn.
73      */
testNoAtime()74     public void testNoAtime() throws Exception {
75         try (BufferedReader br = new BufferedReader(new FileReader("/proc/mounts"))) {
76             String line;
77             while ((line = br.readLine()) != null) {
78                 final String[] fields = line.split(" ");
79                 final String source = fields[0];
80                 final String options = fields[3];
81 
82                 if (source.startsWith("/dev/block/") && !options.startsWith("ro,")
83                         && !options.contains("noatime")) {
84                     fail("Found device mounted at " + source + " without 'noatime' option, "
85                             + "which can cause unnecessary flash churn; please update your fstab.");
86                 }
87             }
88         }
89     }
90 
91     /**
92      * verify hidepid=2 on /proc
93      */
testHidePid2()94     public void testHidePid2() throws Exception {
95         try (BufferedReader br = new BufferedReader(new FileReader("/proc/mounts"))) {
96             String line;
97             while ((line = br.readLine()) != null) {
98                 final String[] fields = line.split(" ");
99                 final String source = fields[0];
100                 final String options = fields[3];
101 
102                 if (source.equals("proc") && !options.contains("hidepid=2")
103                         && !options.contains("hidepid=invisible")) {
104                     fail("proc filesystem mounted without hidepid=2 or hidepid=invisible");
105                 }
106             }
107         }
108     }
109 
testHidePid2_direct()110     public void testHidePid2_direct() throws Exception {
111         assertFalse(new File("/proc/1").exists());
112     }
113 
114     /**
115      * Verify that all writable block filesystems are mounted with "resgid" to
116      * mitigate disk-full trouble.
117      */
testSaneInodes()118     public void testSaneInodes() throws Exception {
119         final File file = Environment.getDataDirectory();
120         final StructStatVfs stat = Os.statvfs(file.getAbsolutePath());
121 
122         // By default ext4 creates one inode per 16KiB; we're okay with a much
123         // wider range, but we want to make sure the device isn't going totally
124         // crazy; too few inodes can result in system instability, and too many
125         // inodes can result in wasted space.
126         final long maxsize = stat.f_blocks * stat.f_frsize;
127         final long maxInodes = maxsize / 4096;
128         // Assuming the smallest storage would be 4GB, min # of free inodes
129         // in EXT4/F2FS must be larger than 128k for Android to work properly.
130         long minInodes = 128 * 1024;
131         final long size4GB = 4294967296l;
132         //If the storage size is smaller than 4GB, let's consider 32k for 1GB.
133         if (maxsize < size4GB) {
134             minInodes = 32 * 1024;
135         }
136 
137         if (stat.f_ffree >= minInodes && stat.f_ffree <= maxInodes
138             && stat.f_favail <= stat.f_ffree) {
139             // Sweet, sounds great!
140         } else {
141             fail("Number of inodes " + stat.f_ffree + "/" + stat.f_favail
142               + " not within sane range for partition of " + maxsize + " bytes; expected ["
143               + minInodes + "," + maxInodes + "]");
144         }
145     }
146 
147     @ApiTest(apis = "android.os.Environment#getDataCePackageDirectoryForUser")
testDataCePackageDirectoryForUser()148     public void testDataCePackageDirectoryForUser() {
149         testDataPackageDirectoryForUser(
150                 (uuid, userHandle) -> Environment.getDataCePackageDirectoryForUser(
151                         uuid, userHandle, sContext.getPackageName()),
152                 (appInfo) -> appInfo.credentialProtectedDataDir
153         );
154     }
155 
156     @ApiTest(apis = "android.os.Environment#getDataDePackageDirectoryForUser")
testDataDePackageDirectoryForUser()157     public void testDataDePackageDirectoryForUser() {
158         testDataPackageDirectoryForUser(
159                 (uuid, userHandle) -> Environment.getDataDePackageDirectoryForUser(
160                         uuid, userHandle, sContext.getPackageName()),
161                 (appInfo) -> appInfo.deviceProtectedDataDir
162         );
163     }
164 
testDataPackageDirectoryForUser( BiFunction<UUID, UserHandle, File> publicApi, Function<ApplicationInfo, String> publicProperty)165     private void testDataPackageDirectoryForUser(
166             BiFunction<UUID, UserHandle, File> publicApi,
167             Function<ApplicationInfo, String> publicProperty) {
168         var appInfo = sContext.getApplicationInfo();
169         // Check that public API is consistent with the public property
170         assertThat(publicApi.apply(appInfo.storageUuid, sContext.getUser()).getAbsolutePath())
171                 .isEqualTo(publicProperty.apply(appInfo));
172     }
173 }
174