1 /*
2  * Copyright (C) 2021 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 
17 package com.android.virt.fs;
18 
19 import static com.android.microdroid.test.host.CommandResultSubject.assertThat;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assume.assumeTrue;
25 
26 import android.platform.test.annotations.RootPermissionTest;
27 
28 import com.android.fs.common.AuthFsTestRule;
29 import com.android.microdroid.test.host.CommandRunner;
30 import com.android.tradefed.device.DeviceNotAvailableException;
31 import com.android.tradefed.invoker.TestInformation;
32 import com.android.tradefed.log.LogUtil.CLog;
33 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
34 import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
35 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
36 import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
37 import com.android.tradefed.util.CommandResult;
38 
39 import org.junit.Rule;
40 import org.junit.Test;
41 import org.junit.runner.RunWith;
42 
43 @RootPermissionTest
44 @RunWith(DeviceJUnit4ClassRunner.class)
45 public final class AuthFsHostTest extends BaseHostJUnit4Test {
46 
47     /** Test directory on Android where data are located */
48     private static final String TEST_DIR = AuthFsTestRule.TEST_DIR;
49 
50     /** Output directory where the test can generate output on Android */
51     private static final String TEST_OUTPUT_DIR = AuthFsTestRule.TEST_OUTPUT_DIR;
52 
53     /** Path to fsverity on Android */
54     private static final String FSVERITY_BIN = "/data/local/tmp/fsverity";
55 
56     /** Mount point of authfs on Microdroid during the test */
57     private static final String MOUNT_DIR = AuthFsTestRule.MOUNT_DIR;
58 
59     /** Input manifest path in the VM. */
60     private static final String INPUT_MANIFEST_PATH = "/mnt/apk/assets/input_manifest.pb";
61 
62     // fs-verity digest (sha256) of testdata/input.{4k, 4k1, 4m}
63     private static final String DIGEST_4K =
64             "sha256-9828cd65f4744d6adda216d3a63d8205375be485bfa261b3b8153d3358f5a576";
65     private static final String DIGEST_4K1 =
66             "sha256-3c70dcd4685ed256ebf1ef116c12e472f35b5017eaca422c0483dadd7d0b5a9f";
67     private static final String DIGEST_4M =
68             "sha256-f18a268d565348fb4bbf11f10480b198f98f2922eb711de149857b3cecf98a8d";
69 
70     private static CommandRunner sAndroid;
71     private static CommandRunner sMicrodroid;
72 
73     @Rule public final AuthFsTestRule mAuthFsTestRule = new AuthFsTestRule();
74 
75     @BeforeClassWithInfo
beforeClassWithDevice(TestInformation testInfo)76     public static void beforeClassWithDevice(TestInformation testInfo) throws Exception {
77         AuthFsTestRule.setUpAndroid(testInfo);
78         assumeTrue(AuthFsTestRule.getDevice().supportsMicrodroid(/*protectedVm=*/ true));
79         AuthFsTestRule.startMicrodroid(/*protectedVm=*/ true);
80         sAndroid = AuthFsTestRule.getAndroid();
81         sMicrodroid = AuthFsTestRule.getMicrodroid();
82     }
83 
84     @AfterClassWithInfo
afterClassWithDevice(TestInformation testInfo)85     public static void afterClassWithDevice(TestInformation testInfo)
86             throws DeviceNotAvailableException {
87         AuthFsTestRule.shutdownMicrodroid();
88         AuthFsTestRule.tearDownAndroid();
89     }
90 
91     @Test
testReadWithFsverityVerification_RemoteFile()92     public void testReadWithFsverityVerification_RemoteFile() throws Exception {
93         // Setup
94         runFdServerOnAndroid(
95                 "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta --open-ro 6:input.4m",
96                 "--ro-fds 3:4 --ro-fds 6");
97         runAuthFsOnMicrodroid("--remote-ro-file-unverified 6 --remote-ro-file 3:" + DIGEST_4M);
98 
99         // Action
100         String actualHashUnverified4m = computeFileHash(sMicrodroid, MOUNT_DIR + "/6");
101         String actualHash4m = computeFileHash(sMicrodroid, MOUNT_DIR + "/3");
102 
103         // Verify
104         String expectedHash4m = computeFileHash(sAndroid, TEST_DIR + "/input.4m");
105 
106         assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4m, actualHashUnverified4m);
107         assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4m, actualHash4m);
108     }
109 
110     // Separate the test from the above simply because exec in shell does not allow open too many
111     // files.
112     @Test
testReadWithFsverityVerification_RemoteSmallerFile()113     public void testReadWithFsverityVerification_RemoteSmallerFile() throws Exception {
114         // Setup
115         runFdServerOnAndroid(
116                 "--open-ro 3:input.4k --open-ro 4:input.4k.fsv_meta --open-ro"
117                     + " 6:input.4k1 --open-ro 7:input.4k1.fsv_meta",
118                 "--ro-fds 3:4 --ro-fds 6:7");
119         runAuthFsOnMicrodroid(
120                 "--remote-ro-file 3:" + DIGEST_4K + " --remote-ro-file 6:" + DIGEST_4K1);
121 
122         // Action
123         String actualHash4k = computeFileHash(sMicrodroid, MOUNT_DIR + "/3");
124         String actualHash4k1 = computeFileHash(sMicrodroid, MOUNT_DIR + "/6");
125 
126         // Verify
127         String expectedHash4k = computeFileHash(sAndroid, TEST_DIR + "/input.4k");
128         String expectedHash4k1 = computeFileHash(sAndroid, TEST_DIR + "/input.4k1");
129 
130         assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4k, actualHash4k);
131         assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4k1, actualHash4k1);
132     }
133 
134     @Test
testReadWithFsverityVerification_TamperedMerkleTree()135     public void testReadWithFsverityVerification_TamperedMerkleTree() throws Exception {
136         // Setup
137         runFdServerOnAndroid(
138                 "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta.bad_merkle",
139                 "--ro-fds 3:4");
140         runAuthFsOnMicrodroid("--remote-ro-file 3:" + DIGEST_4M);
141 
142         // Verify
143         assertThat(copyFile(sMicrodroid, MOUNT_DIR + "/3", "/dev/null")).isFailed();
144     }
145 
146     @Test
testReadWithFsverityVerification_FdServerUsesRealFsverityData()147     public void testReadWithFsverityVerification_FdServerUsesRealFsverityData() throws Exception {
148         // Setup (fs-verity is enabled for input.file in AndroidTest.xml)
149         runFdServerOnAndroid("--open-ro 3:input.file", "--ro-fds 3");
150         String expectedDigest =
151                 sAndroid.run(FSVERITY_BIN + " digest --compact " + TEST_DIR + "/input.file");
152         runAuthFsOnMicrodroid("--remote-ro-file 3:sha256-" + expectedDigest);
153 
154         // Action
155         String actualHash = computeFileHash(sMicrodroid, MOUNT_DIR + "/3");
156 
157         // Verify
158         String expectedHash = computeFileHash(sAndroid, TEST_DIR + "/input.file");
159         assertEquals("Inconsistent hash from /authfs/3: ", expectedHash, actualHash);
160     }
161 
162     @Test
testWriteThroughCorrectly()163     public void testWriteThroughCorrectly() throws Exception {
164         // Setup
165         runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
166         runAuthFsOnMicrodroid("--remote-new-rw-file 3");
167 
168         // Action
169         String srcPath = "/system/bin/linker64";
170         String destPath = MOUNT_DIR + "/3";
171         String backendPath = TEST_OUTPUT_DIR + "/out.file";
172         assertThat(copyFile(sMicrodroid, srcPath, destPath)).isSuccess();
173 
174         // Verify
175         String expectedHash = computeFileHash(sMicrodroid, srcPath);
176         expectBackingFileConsistency(destPath, backendPath, expectedHash);
177     }
178 
179     @Test
testWriteFailedIfDetectsTampering()180     public void testWriteFailedIfDetectsTampering() throws Exception {
181         // Setup
182         runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
183         runAuthFsOnMicrodroid("--remote-new-rw-file 3");
184 
185         String srcPath = "/system/bin/linker64";
186         String destPath = MOUNT_DIR + "/3";
187         String backendPath = TEST_OUTPUT_DIR + "/out.file";
188         assertThat(copyFile(sMicrodroid, srcPath, destPath)).isSuccess();
189 
190         // Action
191         // Tampering with the first 2 4K-blocks of the backing file.
192         assertThat(
193                 writeZerosAtFileOffset(sAndroid, backendPath,
194                         /* offset */ 0, /* number */ 8192, /* writeThrough */ false))
195                 .isSuccess();
196 
197         // Verify
198         // Write to a block partially requires a read back to calculate the new hash. It should fail
199         // when the content is inconsistent to the known hash. Use direct I/O to avoid simply
200         // writing to the filesystem cache.
201         assertThat(
202                 writeZerosAtFileOffset(sMicrodroid, destPath,
203                         /* offset */ 0, /* number */ 1024, /* writeThrough */ true))
204                 .isFailed();
205 
206         // A full 4K write does not require to read back, so write can succeed even if the backing
207         // block has already been tampered.
208         assertThat(
209                 writeZerosAtFileOffset(sMicrodroid, destPath,
210                         /* offset */ 4096, /* number */ 4096, /* writeThrough */ false))
211                 .isSuccess();
212 
213         // Otherwise, a partial write with correct backing file should still succeed.
214         assertThat(
215                 writeZerosAtFileOffset(sMicrodroid, destPath,
216                         /* offset */ 8192, /* number */ 1024, /* writeThrough */ false))
217                 .isSuccess();
218     }
219 
220     @Test
testReadFailedIfDetectsTampering()221     public void testReadFailedIfDetectsTampering() throws Exception {
222         // Setup
223         runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
224         runAuthFsOnMicrodroid("--remote-new-rw-file 3");
225 
226         String srcPath = "/system/bin/linker64";
227         String destPath = MOUNT_DIR + "/3";
228         String backendPath = TEST_OUTPUT_DIR + "/out.file";
229         assertThat(copyFile(sMicrodroid, srcPath, destPath)).isSuccess();
230 
231         // Action
232         // Tampering with the first 4K-block of the backing file.
233         assertThat(
234                 writeZerosAtFileOffset(sAndroid, backendPath,
235                         /* offset */ 0, /* number */ 4096, /* writeThrough */ false))
236                 .isSuccess();
237 
238         // Verify
239         // Force dropping the page cache, so that the next read can be validated.
240         sMicrodroid.run("echo 1 > /proc/sys/vm/drop_caches");
241         // A read will fail if the backing data has been tampered.
242         assertThat(checkReadAt(sMicrodroid, destPath, /* offset */ 0, /* number */ 4096))
243                 .isFailed();
244         assertThat(checkReadAt(sMicrodroid, destPath, /* offset */ 4096, /* number */ 4096))
245                 .isSuccess();
246     }
247 
248     @Test
testResizeFailedIfDetectsTampering()249     public void testResizeFailedIfDetectsTampering() throws Exception {
250         // Setup
251         runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
252         runAuthFsOnMicrodroid("--remote-new-rw-file 3");
253 
254         String outputPath = MOUNT_DIR + "/3";
255         String backendPath = TEST_OUTPUT_DIR + "/out.file";
256         createFileWithOnes(sMicrodroid, outputPath, 8192);
257 
258         // Action
259         // Tampering with the last 4K-block of the backing file.
260         assertThat(
261                 writeZerosAtFileOffset(sAndroid, backendPath,
262                         /* offset */ 4096, /* number */ 1, /* writeThrough */ false))
263                 .isSuccess();
264 
265         // Verify
266         // A resize (to a non-multiple of 4K) will fail if the last backing chunk has been
267         // tampered. The original data is necessary (and has to be verified) to calculate the new
268         // hash with shorter data.
269         assertThat(resizeFile(sMicrodroid, outputPath, 8000)).isFailed();
270     }
271 
272     @Test
testFileResize()273     public void testFileResize() throws Exception {
274         // Setup
275         runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
276         runAuthFsOnMicrodroid("--remote-new-rw-file 3");
277         String outputPath = MOUNT_DIR + "/3";
278         String backendPath = TEST_OUTPUT_DIR + "/out.file";
279 
280         // Action & Verify
281         createFileWithOnes(sMicrodroid, outputPath, 10000);
282         assertEquals(getFileSizeInBytes(sMicrodroid, outputPath), 10000);
283         expectBackingFileConsistency(
284                 outputPath,
285                 backendPath,
286                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
287 
288         assertThat(resizeFile(sMicrodroid, outputPath, 15000)).isSuccess();
289         assertEquals(getFileSizeInBytes(sMicrodroid, outputPath), 15000);
290         expectBackingFileConsistency(
291                 outputPath,
292                 backendPath,
293                 "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d");
294 
295         assertThat(resizeFile(sMicrodroid, outputPath, 5000)).isSuccess();
296         assertEquals(getFileSizeInBytes(sMicrodroid, outputPath), 5000);
297         expectBackingFileConsistency(
298                 outputPath,
299                 backendPath,
300                 "e53130831c13dabff71d5d1797e3aaa467b4b7d32b3b8782c4ff03d76976f2aa");
301     }
302 
303     @Test
testOutputDirectory_WriteNewFiles()304     public void testOutputDirectory_WriteNewFiles() throws Exception {
305         // Setup
306         String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
307         String authfsOutputDir = MOUNT_DIR + "/3";
308         sAndroid.run("mkdir " + androidOutputDir);
309         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
310         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
311 
312         // Action & Verify
313         // Can create a new file to write.
314         String expectedAndroidPath = androidOutputDir + "/file";
315         String authfsPath = authfsOutputDir + "/file";
316         createFileWithOnes(sMicrodroid, authfsPath, 10000);
317         assertEquals(getFileSizeInBytes(sMicrodroid, authfsPath), 10000);
318         expectBackingFileConsistency(
319                 authfsPath,
320                 expectedAndroidPath,
321                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
322 
323         // Regular file operations work, e.g. resize.
324         assertThat(resizeFile(sMicrodroid, authfsPath, 15000)).isSuccess();
325         assertEquals(getFileSizeInBytes(sMicrodroid, authfsPath), 15000);
326         expectBackingFileConsistency(
327                 authfsPath,
328                 expectedAndroidPath,
329                 "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d");
330     }
331 
332     @Test
testOutputDirectory_MkdirAndWriteFile()333     public void testOutputDirectory_MkdirAndWriteFile() throws Exception {
334         // Setup
335         String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
336         String authfsOutputDir = MOUNT_DIR + "/3";
337         sAndroid.run("mkdir " + androidOutputDir);
338         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
339         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
340 
341         // Action
342         // Can create nested directories and can create a file in one.
343         sMicrodroid.run("mkdir " + authfsOutputDir + "/new_dir");
344         sMicrodroid.run("mkdir -p " + authfsOutputDir + "/we/need/to/go/deeper");
345         createFileWithOnes(sMicrodroid, authfsOutputDir + "/new_dir/file1", 10000);
346         createFileWithOnes(sMicrodroid, authfsOutputDir + "/we/need/file2", 10000);
347 
348         // Verify
349         // Directories show up in Android.
350         sAndroid.run("test -d " + androidOutputDir + "/new_dir");
351         sAndroid.run("test -d " + androidOutputDir + "/we/need/to/go/deeper");
352         // Files exist in Android. Hashes on Microdroid and Android are consistent.
353         assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/new_dir/file1"), 10000);
354         expectBackingFileConsistency(
355                 authfsOutputDir + "/new_dir/file1",
356                 androidOutputDir + "/new_dir/file1",
357                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
358         // Same to file in a nested directory.
359         assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/we/need/file2"), 10000);
360         expectBackingFileConsistency(
361                 authfsOutputDir + "/we/need/file2",
362                 androidOutputDir + "/we/need/file2",
363                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
364     }
365 
366     @Test
testOutputDirectory_CreateAndTruncateExistingFile()367     public void testOutputDirectory_CreateAndTruncateExistingFile() throws Exception {
368         // Setup
369         String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
370         String authfsOutputDir = MOUNT_DIR + "/3";
371         sAndroid.run("mkdir " + androidOutputDir);
372         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
373         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
374 
375         // Action & Verify
376         sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/file");
377         assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/file"), 3);
378         // Can override a file and write normally.
379         createFileWithOnes(sMicrodroid, authfsOutputDir + "/file", 10000);
380         assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/file"), 10000);
381         expectBackingFileConsistency(
382                 authfsOutputDir + "/file",
383                 androidOutputDir + "/file",
384                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
385     }
386 
387     @Test
testOutputDirectory_CanDeleteFile()388     public void testOutputDirectory_CanDeleteFile() throws Exception {
389         // Setup
390         String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
391         String authfsOutputDir = MOUNT_DIR + "/3";
392         sAndroid.run("mkdir " + androidOutputDir);
393         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
394         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
395 
396         sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/file");
397         sMicrodroid.run("test -f " + authfsOutputDir + "/file");
398         sAndroid.run("test -f " + androidOutputDir + "/file");
399 
400         // Action & Verify
401         sMicrodroid.run("rm " + authfsOutputDir + "/file");
402         sMicrodroid.run("test ! -f " + authfsOutputDir + "/file");
403         sAndroid.run("test ! -f " + androidOutputDir + "/file");
404     }
405 
406     @Test
testOutputDirectory_CanDeleteDirectoryOnlyIfEmpty()407     public void testOutputDirectory_CanDeleteDirectoryOnlyIfEmpty() throws Exception {
408         // Setup
409         String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
410         String authfsOutputDir = MOUNT_DIR + "/3";
411         sAndroid.run("mkdir " + androidOutputDir);
412         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
413         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
414 
415         sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir/dir2");
416         sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/dir/file");
417         sAndroid.run("test -d " + androidOutputDir + "/dir/dir2");
418 
419         // Action & Verify
420         sMicrodroid.run("rmdir " + authfsOutputDir + "/dir/dir2");
421         sMicrodroid.run("test ! -d " + authfsOutputDir + "/dir/dir2");
422         sAndroid.run("test ! -d " + androidOutputDir + "/dir/dir2");
423         // Can only delete a directory if empty
424         assertThat(sMicrodroid.runForResult("rmdir " + authfsOutputDir + "/dir")).isFailed();
425         sMicrodroid.run("test -d " + authfsOutputDir + "/dir"); // still there
426         sMicrodroid.run("rm " + authfsOutputDir + "/dir/file");
427         sMicrodroid.run("rmdir " + authfsOutputDir + "/dir");
428         sMicrodroid.run("test ! -d " + authfsOutputDir + "/dir");
429         sAndroid.run("test ! -d " + androidOutputDir + "/dir");
430     }
431 
432     @Test
testOutputDirectory_CannotRecreateDirectoryIfNameExists()433     public void testOutputDirectory_CannotRecreateDirectoryIfNameExists() throws Exception {
434         // Setup
435         String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
436         String authfsOutputDir = MOUNT_DIR + "/3";
437         sAndroid.run("mkdir " + androidOutputDir);
438         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
439         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
440 
441         sMicrodroid.run("touch " + authfsOutputDir + "/some_file");
442         sMicrodroid.run("mkdir " + authfsOutputDir + "/some_dir");
443         sMicrodroid.run("touch " + authfsOutputDir + "/some_dir/file");
444         sMicrodroid.run("mkdir " + authfsOutputDir + "/some_dir/dir");
445 
446         // Action & Verify
447         // Cannot create directory if an entry with the same name already exists.
448         assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_file")).isFailed();
449         assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_dir")).isFailed();
450         assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_dir/file"))
451                 .isFailed();
452         assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_dir/dir"))
453                 .isFailed();
454     }
455 
456     @Test
testOutputDirectory_WriteToFdOfDeletedFile()457     public void testOutputDirectory_WriteToFdOfDeletedFile() throws Exception {
458         // Setup
459         String authfsOutputDir = MOUNT_DIR + "/3";
460         String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
461         sAndroid.run("mkdir " + androidOutputDir);
462         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
463         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
464 
465         // Create a file with some data. Test the existence.
466         String outputPath = authfsOutputDir + "/out";
467         String androidOutputPath = androidOutputDir + "/out";
468         sMicrodroid.run("echo -n 123 > " + outputPath);
469         sMicrodroid.run("test -f " + outputPath);
470         sAndroid.run("test -f " + androidOutputPath);
471 
472         // Action
473         String output = sMicrodroid.run(
474                 // Open the file for append and read
475                 "exec 4>>" + outputPath + " 5<" + outputPath + "; "
476                 // Delete the file from the directory
477                 + "rm " + outputPath + "; "
478                 // Append more data to the file descriptor
479                 + "echo -n 456 >&4; "
480                 // Print the whole file from the file descriptor
481                 + "cat <&5");
482 
483         // Verify
484         // Output contains all written data, while the files are deleted.
485         assertEquals("123456", output);
486         sMicrodroid.run("test ! -f " + outputPath);
487         sAndroid.run("test ! -f " + androidOutputDir + "/out");
488     }
489 
490     @Test
testInputDirectory_CanReadFile()491     public void testInputDirectory_CanReadFile() throws Exception {
492         // Setup
493         String authfsInputDir = MOUNT_DIR + "/3";
494         runFdServerOnAndroid("--open-dir 3:" + TEST_DIR, "--ro-dirs 3");
495         runAuthFsOnMicrodroid("--remote-ro-dir 3:" + INPUT_MANIFEST_PATH + ":");
496 
497         // Action
498         String actualHash = computeFileHash(sMicrodroid, authfsInputDir + "/input.4m");
499 
500         // Verify
501         String expectedHash = computeFileHash(sAndroid, TEST_DIR + "/input.4m");
502         assertEquals("Expect consistent hash through /authfs/3: ", expectedHash, actualHash);
503     }
504 
505     @Test
testInputDirectory_OnlyAllowlistedFilesExist()506     public void testInputDirectory_OnlyAllowlistedFilesExist() throws Exception {
507         // Setup
508         String authfsInputDir = MOUNT_DIR + "/3";
509         runFdServerOnAndroid("--open-dir 3:" + TEST_DIR, "--ro-dirs 3");
510         runAuthFsOnMicrodroid("--remote-ro-dir 3:" + INPUT_MANIFEST_PATH + ":");
511 
512         // Verify
513         sMicrodroid.run("test -f " + authfsInputDir + "/input.4k");
514         assertThat(sMicrodroid.runForResult("test -f " + authfsInputDir + "/input.4k.fsv_meta"))
515                 .isFailed();
516     }
517 
518     @Test
testReadOutputDirectory()519     public void testReadOutputDirectory() throws Exception {
520         // Setup
521         runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3");
522         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
523 
524         // Action
525         String authfsOutputDir = MOUNT_DIR + "/3";
526         sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir/dir2/dir3");
527         sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file1");
528         sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file2");
529         sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file3");
530         sMicrodroid.run("touch " + authfsOutputDir + "/file");
531 
532         // Verify
533         String[] actual = sMicrodroid.run("cd " + authfsOutputDir + "; find |sort").split("\n");
534         String[] expected = new String[] {
535                 ".",
536                 "./dir",
537                 "./dir/dir2",
538                 "./dir/dir2/dir3",
539                 "./dir/dir2/dir3/file1",
540                 "./dir/dir2/dir3/file2",
541                 "./dir/dir2/dir3/file3",
542                 "./file"};
543         assertEquals(expected, actual);
544 
545         // Add more entries.
546         sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir2");
547         sMicrodroid.run("touch " + authfsOutputDir + "/file2");
548         // Check new entries. Also check that the types are correct.
549         actual = sMicrodroid.run(
550                 "cd " + authfsOutputDir + "; find -maxdepth 1 -type f |sort").split("\n");
551         expected = new String[] {"./file", "./file2"};
552         assertEquals(expected, actual);
553         actual = sMicrodroid.run(
554                 "cd " + authfsOutputDir + "; find -maxdepth 1 -type d |sort").split("\n");
555         expected = new String[] {".", "./dir", "./dir2"};
556         assertEquals(expected, actual);
557     }
558 
559     @Test
testChmod_File()560     public void testChmod_File() throws Exception {
561         // Setup
562         runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/file", "--rw-fds 3");
563         runAuthFsOnMicrodroid("--remote-new-rw-file 3");
564 
565         // Action & Verify
566         // Change mode
567         sMicrodroid.run("chmod 321 " + MOUNT_DIR + "/3");
568         expectFileMode("--wx-w---x", MOUNT_DIR + "/3", TEST_OUTPUT_DIR + "/file");
569         // Can't set the disallowed bits
570         assertThat(sMicrodroid.runForResult("chmod +s " + MOUNT_DIR + "/3")).isFailed();
571         assertThat(sMicrodroid.runForResult("chmod +t " + MOUNT_DIR + "/3")).isFailed();
572     }
573 
574     @Test
testChmod_Dir()575     public void testChmod_Dir() throws Exception {
576         // Setup
577         runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3");
578         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
579 
580         // Action & Verify
581         String authfsOutputDir = MOUNT_DIR + "/3";
582         // Create with umask
583         sMicrodroid.run("umask 000; mkdir " + authfsOutputDir + "/dir");
584         sMicrodroid.run("umask 022; mkdir " + authfsOutputDir + "/dir/dir2");
585         expectFileMode("drwxrwxrwx", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir");
586         expectFileMode("drwxr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2");
587         // Change mode
588         sMicrodroid.run("chmod -w " + authfsOutputDir + "/dir/dir2");
589         expectFileMode("dr-xr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2");
590         sMicrodroid.run("chmod 321 " + authfsOutputDir + "/dir");
591         expectFileMode("d-wx-w---x", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir");
592         // Can't set the disallowed bits
593         assertThat(sMicrodroid.runForResult("chmod +s " + authfsOutputDir + "/dir/dir2"))
594                 .isFailed();
595         assertThat(sMicrodroid.runForResult("chmod +t " + authfsOutputDir + "/dir")).isFailed();
596     }
597 
598     @Test
testChmod_FileInOutputDirectory()599     public void testChmod_FileInOutputDirectory() throws Exception {
600         // Setup
601         runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3");
602         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
603 
604         // Action & Verify
605         String authfsOutputDir = MOUNT_DIR + "/3";
606         // Create with umask
607         sMicrodroid.run("umask 000; echo -n foo > " + authfsOutputDir + "/file");
608         sMicrodroid.run("umask 022; echo -n foo > " + authfsOutputDir + "/file2");
609         expectFileMode("-rw-rw-rw-", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file");
610         expectFileMode("-rw-r--r--", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2");
611         // Change mode
612         sMicrodroid.run("chmod -w " + authfsOutputDir + "/file");
613         expectFileMode("-r--r--r--", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file");
614         sMicrodroid.run("chmod 321 " + authfsOutputDir + "/file2");
615         expectFileMode("--wx-w---x", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2");
616         // Can't set the disallowed bits
617         assertThat(sMicrodroid.runForResult("chmod +s " + authfsOutputDir + "/file")).isFailed();
618         assertThat(sMicrodroid.runForResult("chmod +t " + authfsOutputDir + "/file2")).isFailed();
619     }
620 
621     @Test
testStatfs()622     public void testStatfs() throws Exception {
623         // Setup
624         runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3");
625         runAuthFsOnMicrodroid("--remote-new-rw-dir 3");
626 
627         // Verify
628         // Magic matches. Has only 2 inodes (root and "/3").
629         assertEquals(
630                 mAuthFsTestRule.FUSE_SUPER_MAGIC_HEX + " 2",
631                 sMicrodroid.run("stat -f -c '%t %c' " + MOUNT_DIR));
632     }
633 
expectBackingFileConsistency( String authFsPath, String backendPath, String expectedHash)634     private void expectBackingFileConsistency(
635             String authFsPath, String backendPath, String expectedHash)
636             throws DeviceNotAvailableException {
637         String hashOnAuthFs = computeFileHash(sMicrodroid, authFsPath);
638         assertEquals("File hash is different to expectation", expectedHash, hashOnAuthFs);
639 
640         String hashOfBackingFile = computeFileHash(sAndroid, backendPath);
641         assertEquals(
642                 "Inconsistent file hash on the backend storage", hashOnAuthFs, hashOfBackingFile);
643     }
644 
computeFileHash(CommandRunner runner, String path)645     private static String computeFileHash(CommandRunner runner, String path)
646             throws DeviceNotAvailableException {
647         String result = runner.run("sha256sum " + path);
648         String[] tokens = result.split("\\s");
649         if (tokens.length > 0) {
650             return tokens[0];
651         } else {
652             CLog.e("Unrecognized output by sha256sum: " + result);
653             return "";
654         }
655     }
656 
copyFile(CommandRunner runner, String src, String dest)657     private static CommandResult copyFile(CommandRunner runner, String src, String dest)
658             throws DeviceNotAvailableException {
659         // toybox's cp(1) implementation ignores most read(2) errors, and it's unclear what the
660         // canonical behavior should be (not mentioned in manpage). For this test, use cat(1) in
661         // order to fail on I/O error.
662         return runner.runForResult("cat " + src + " > " + dest);
663     }
664 
expectFileMode(String expected, String microdroidPath, String androidPath)665     private void expectFileMode(String expected, String microdroidPath, String androidPath)
666             throws DeviceNotAvailableException {
667         String actual = sMicrodroid.run("stat -c '%A' " + microdroidPath);
668         assertEquals("Inconsistent mode for " + microdroidPath, expected, actual);
669 
670         actual = sAndroid.run("stat -c '%A' " + androidPath);
671         assertEquals("Inconsistent mode for " + androidPath + " (android)", expected, actual);
672     }
673 
resizeFile(CommandRunner runner, String path, long size)674     private static CommandResult resizeFile(CommandRunner runner, String path, long size)
675             throws DeviceNotAvailableException {
676         return runner.runForResult("truncate -c -s " + size + " " + path);
677     }
678 
getFileSizeInBytes(CommandRunner runner, String path)679     private static long getFileSizeInBytes(CommandRunner runner, String path)
680             throws DeviceNotAvailableException {
681         return Long.parseLong(runner.run("stat -c '%s' " + path));
682     }
683 
createFileWithOnes(CommandRunner runner, String filePath, long numberOfOnes)684     private static void createFileWithOnes(CommandRunner runner, String filePath, long numberOfOnes)
685             throws DeviceNotAvailableException {
686         runner.run(
687                 "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=" + numberOfOnes + " of=" + filePath);
688     }
689 
checkReadAt(CommandRunner runner, String filePath, long offset, long size)690     private static CommandResult checkReadAt(CommandRunner runner, String filePath, long offset,
691             long size) throws DeviceNotAvailableException {
692         String cmd = "dd if=" + filePath + " of=/dev/null bs=1 count=" + size;
693         if (offset > 0) {
694             cmd += " skip=" + offset;
695         }
696         return runner.runForResult(cmd);
697     }
698 
writeZerosAtFileOffset(CommandRunner runner, String filePath, long offset, long numberOfZeros, boolean writeThrough)699     private CommandResult writeZerosAtFileOffset(CommandRunner runner, String filePath, long offset,
700             long numberOfZeros, boolean writeThrough) throws DeviceNotAvailableException {
701         String cmd = "dd if=/dev/zero of=" + filePath + " bs=1 count=" + numberOfZeros
702                 + " conv=notrunc";
703         if (offset > 0) {
704             cmd += " seek=" + offset;
705         }
706         if (writeThrough) {
707             cmd += " direct";
708         }
709         return runner.runForResult(cmd);
710     }
711 
runAuthFsOnMicrodroid(String flags)712     private void runAuthFsOnMicrodroid(String flags) {
713         mAuthFsTestRule.runAuthFsOnMicrodroid(flags);
714     }
715 
runFdServerOnAndroid(String helperFlags, String fdServerFlags)716     private void runFdServerOnAndroid(String helperFlags, String fdServerFlags)
717             throws DeviceNotAvailableException {
718         mAuthFsTestRule.runFdServerOnAndroid(helperFlags, fdServerFlags);
719     }
720 }
721