1 /*
2  * Copyright (C) 2019 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.apkverity;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24 
25 import android.platform.test.annotations.RootPermissionTest;
26 
27 import com.android.blockdevicewriter.BlockDeviceWriter;
28 import com.android.tradefed.device.DeviceNotAvailableException;
29 import com.android.tradefed.device.ITestDevice;
30 import com.android.tradefed.log.LogUtil.CLog;
31 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
32 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
33 import com.android.tradefed.util.CommandResult;
34 import com.android.tradefed.util.CommandStatus;
35 
36 import org.junit.After;
37 import org.junit.Before;
38 import org.junit.Test;
39 import org.junit.runner.RunWith;
40 
41 import java.io.FileNotFoundException;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.HashSet;
45 import java.util.Set;
46 
47 /**
48  * This test makes sure app installs with fs-verity signature, and on-access verification works.
49  *
50  * <p>When an app is installed, all or none of the files should have their corresponding .fsv_sig
51  * signature file. Otherwise, install will fail.
52  *
53  * <p>Once installed, file protected by fs-verity is verified by kernel every time a block is loaded
54  * from disk to memory. The file is immutable by design, enforced by filesystem.
55  *
56  * <p>In order to make sure a block of the file is readable only if the underlying block on disk
57  * stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical
58  * address against the block device.
59  *
60  * <p>Requirements to run this test:
61  * <ul>
62  *   <li>Device is rootable</li>
63  *   <li>The filesystem supports fs-verity</li>
64  *   <li>The feature flag is enabled</li>
65  * </ul>
66  */
67 @RootPermissionTest
68 @RunWith(DeviceJUnit4ClassRunner.class)
69 public class ApkVerityTest extends BaseHostJUnit4Test {
70     private static final String TARGET_PACKAGE = "com.android.apkverity";
71 
72     private static final String BASE_APK = "ApkVerityTestApp.apk";
73     private static final String BASE_APK_DM = "ApkVerityTestApp.dm";
74     private static final String SPLIT_APK = "ApkVerityTestAppSplit.apk";
75     private static final String SPLIT_APK_DM = "ApkVerityTestAppSplit.dm";
76 
77     private static final String INSTALLED_BASE_APK = "base.apk";
78     private static final String INSTALLED_BASE_DM = "base.dm";
79     private static final String INSTALLED_SPLIT_APK = "split_feature_x.apk";
80     private static final String INSTALLED_SPLIT_DM = "split_feature_x.dm";
81     private static final String INSTALLED_BASE_APK_FSV_SIG = "base.apk.fsv_sig";
82     private static final String INSTALLED_BASE_DM_FSV_SIG = "base.dm.fsv_sig";
83     private static final String INSTALLED_SPLIT_APK_FSV_SIG = "split_feature_x.apk.fsv_sig";
84     private static final String INSTALLED_SPLIT_DM_FSV_SIG = "split_feature_x.dm.fsv_sig";
85 
86     private static final String DAMAGING_EXECUTABLE = "/data/local/tmp/block_device_writer";
87     private static final String CERT_PATH = "/data/local/tmp/ApkVerityTestCert.der";
88 
89     /** Only 4K page is supported by fs-verity currently. */
90     private static final int FSVERITY_PAGE_SIZE = 4096;
91 
92     private ITestDevice mDevice;
93     private boolean mDmRequireFsVerity;
94 
95     @Before
setUp()96     public void setUp() throws DeviceNotAvailableException {
97         mDevice = getDevice();
98         mDmRequireFsVerity = "true".equals(
99                 mDevice.getProperty("pm.dexopt.dm.require_fsverity"));
100 
101         expectRemoteCommandToSucceed("cmd file_integrity append-cert " + CERT_PATH);
102         uninstallPackage(TARGET_PACKAGE);
103     }
104 
105     @After
tearDown()106     public void tearDown() throws DeviceNotAvailableException {
107         expectRemoteCommandToSucceed("cmd file_integrity remove-last-cert");
108         uninstallPackage(TARGET_PACKAGE);
109     }
110 
111     @Test
testFsverityKernelSupports()112     public void testFsverityKernelSupports() throws DeviceNotAvailableException {
113         ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data");
114         expectRemoteCommandToSucceed("test -f /sys/fs/" + mountPoint.type + "/features/verity");
115     }
116 
117     @Test
testInstallBase()118     public void testInstallBase() throws DeviceNotAvailableException, FileNotFoundException {
119         new InstallMultiple()
120                 .addFileAndSignature(BASE_APK)
121                 .run();
122         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
123 
124         verifyInstalledFiles(
125                 INSTALLED_BASE_APK,
126                 INSTALLED_BASE_APK_FSV_SIG);
127         verifyInstalledFilesHaveFsverity(INSTALLED_BASE_APK);
128     }
129 
130     @Test
testInstallBaseWithWrongSignature()131     public void testInstallBaseWithWrongSignature()
132             throws DeviceNotAvailableException, FileNotFoundException {
133         new InstallMultiple()
134                 .addFile(BASE_APK)
135                 .addFile(SPLIT_APK_DM + ".fsv_sig",
136                         BASE_APK + ".fsv_sig")
137                 .runExpectingFailure();
138     }
139 
140     @Test
testInstallBaseWithSplit()141     public void testInstallBaseWithSplit()
142             throws DeviceNotAvailableException, FileNotFoundException {
143         new InstallMultiple()
144                 .addFileAndSignature(BASE_APK)
145                 .addFileAndSignature(SPLIT_APK)
146                 .run();
147         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
148 
149         verifyInstalledFiles(
150                 INSTALLED_BASE_APK,
151                 INSTALLED_BASE_APK_FSV_SIG,
152                 INSTALLED_SPLIT_APK,
153                 INSTALLED_SPLIT_APK_FSV_SIG);
154         verifyInstalledFilesHaveFsverity(
155                 INSTALLED_BASE_APK,
156                 INSTALLED_SPLIT_APK);
157     }
158 
159     @Test
testInstallBaseWithDm()160     public void testInstallBaseWithDm() throws DeviceNotAvailableException, FileNotFoundException {
161         new InstallMultiple()
162                 .addFileAndSignature(BASE_APK)
163                 .addFileAndSignature(BASE_APK_DM)
164                 .run();
165         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
166 
167         verifyInstalledFiles(
168                 INSTALLED_BASE_APK,
169                 INSTALLED_BASE_APK_FSV_SIG,
170                 INSTALLED_BASE_DM,
171                 INSTALLED_BASE_DM_FSV_SIG);
172         verifyInstalledFilesHaveFsverity(
173                 INSTALLED_BASE_APK,
174                 INSTALLED_BASE_DM);
175     }
176 
177     @Test
testInstallEverything()178     public void testInstallEverything() throws DeviceNotAvailableException, FileNotFoundException {
179         new InstallMultiple()
180                 .addFileAndSignature(BASE_APK)
181                 .addFileAndSignature(BASE_APK_DM)
182                 .addFileAndSignature(SPLIT_APK)
183                 .addFileAndSignature(SPLIT_APK_DM)
184                 .run();
185         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
186 
187         verifyInstalledFiles(
188                 INSTALLED_BASE_APK,
189                 INSTALLED_BASE_APK_FSV_SIG,
190                 INSTALLED_BASE_DM,
191                 INSTALLED_BASE_DM_FSV_SIG,
192                 INSTALLED_SPLIT_APK,
193                 INSTALLED_SPLIT_APK_FSV_SIG,
194                 INSTALLED_SPLIT_DM,
195                 INSTALLED_SPLIT_DM_FSV_SIG);
196         verifyInstalledFilesHaveFsverity(
197                 INSTALLED_BASE_APK,
198                 INSTALLED_BASE_DM,
199                 INSTALLED_SPLIT_APK,
200                 INSTALLED_SPLIT_DM);
201     }
202 
203     @Test
testInstallSplitOnly()204     public void testInstallSplitOnly()
205             throws DeviceNotAvailableException, FileNotFoundException {
206         new InstallMultiple()
207                 .addFileAndSignature(BASE_APK)
208                 .run();
209         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
210         verifyInstalledFiles(
211                 INSTALLED_BASE_APK,
212                 INSTALLED_BASE_APK_FSV_SIG);
213 
214         new InstallMultiple()
215                 .inheritFrom(TARGET_PACKAGE)
216                 .addFileAndSignature(SPLIT_APK)
217                 .run();
218 
219         verifyInstalledFiles(
220                 INSTALLED_BASE_APK,
221                 INSTALLED_BASE_APK_FSV_SIG,
222                 INSTALLED_SPLIT_APK,
223                 INSTALLED_SPLIT_APK_FSV_SIG);
224         verifyInstalledFilesHaveFsverity(
225                 INSTALLED_BASE_APK,
226                 INSTALLED_SPLIT_APK);
227     }
228 
229     @Test
testInstallSplitOnlyMissingSignature()230     public void testInstallSplitOnlyMissingSignature()
231             throws DeviceNotAvailableException, FileNotFoundException {
232         new InstallMultiple()
233                 .addFileAndSignature(BASE_APK)
234                 .run();
235         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
236         verifyInstalledFiles(
237                 INSTALLED_BASE_APK,
238                 INSTALLED_BASE_APK_FSV_SIG);
239 
240         new InstallMultiple()
241                 .inheritFrom(TARGET_PACKAGE)
242                 .addFile(SPLIT_APK)
243                 .runExpectingFailure();
244     }
245 
246     @Test
testInstallSplitOnlyWithoutBaseSignature()247     public void testInstallSplitOnlyWithoutBaseSignature()
248             throws DeviceNotAvailableException, FileNotFoundException {
249         new InstallMultiple()
250                 .addFile(BASE_APK)
251                 .run();
252         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
253         verifyInstalledFiles(INSTALLED_BASE_APK);
254 
255         new InstallMultiple()
256                 .inheritFrom(TARGET_PACKAGE)
257                 .addFileAndSignature(SPLIT_APK)
258                 .run();
259         verifyInstalledFiles(
260                 INSTALLED_BASE_APK,
261                 INSTALLED_SPLIT_APK,
262                 INSTALLED_SPLIT_APK_FSV_SIG);
263     }
264 
265     @Test
testInstallOnlyDmHasFsvSig()266     public void testInstallOnlyDmHasFsvSig()
267             throws DeviceNotAvailableException, FileNotFoundException {
268         new InstallMultiple()
269                 .addFile(BASE_APK)
270                 .addFileAndSignature(BASE_APK_DM)
271                 .addFile(SPLIT_APK)
272                 .addFileAndSignature(SPLIT_APK_DM)
273                 .run();
274         verifyInstalledFiles(
275                 INSTALLED_BASE_APK,
276                 INSTALLED_BASE_DM,
277                 INSTALLED_BASE_DM_FSV_SIG,
278                 INSTALLED_SPLIT_APK,
279                 INSTALLED_SPLIT_DM,
280                 INSTALLED_SPLIT_DM_FSV_SIG);
281         verifyInstalledFilesHaveFsverity(
282                 INSTALLED_BASE_DM,
283                 INSTALLED_SPLIT_DM);
284     }
285 
286     @Test
testInstallDmWithoutFsvSig_Base()287     public void testInstallDmWithoutFsvSig_Base()
288             throws DeviceNotAvailableException, FileNotFoundException {
289         InstallMultiple installer = new InstallMultiple()
290                 .addFile(BASE_APK)
291                 .addFile(BASE_APK_DM)
292                 .addFile(SPLIT_APK)
293                 .addFileAndSignature(SPLIT_APK_DM);
294         if (mDmRequireFsVerity) {
295             installer.runExpectingFailure();
296         } else {
297             installer.run();
298             verifyInstalledFiles(
299                     INSTALLED_BASE_APK,
300                     INSTALLED_BASE_DM,
301                     INSTALLED_SPLIT_APK,
302                     INSTALLED_SPLIT_DM,
303                     INSTALLED_SPLIT_DM_FSV_SIG);
304             verifyInstalledFilesHaveFsverity(INSTALLED_SPLIT_DM);
305         }
306     }
307 
308     @Test
testInstallDmWithoutFsvSig_Split()309     public void testInstallDmWithoutFsvSig_Split()
310             throws DeviceNotAvailableException, FileNotFoundException {
311         InstallMultiple installer = new InstallMultiple()
312                 .addFile(BASE_APK)
313                 .addFileAndSignature(BASE_APK_DM)
314                 .addFile(SPLIT_APK)
315                 .addFile(SPLIT_APK_DM);
316         if (mDmRequireFsVerity) {
317             installer.runExpectingFailure();
318         } else {
319             installer.run();
320             verifyInstalledFiles(
321                     INSTALLED_BASE_APK,
322                     INSTALLED_BASE_DM,
323                     INSTALLED_BASE_DM_FSV_SIG,
324                     INSTALLED_SPLIT_APK,
325                     INSTALLED_SPLIT_DM);
326             verifyInstalledFilesHaveFsverity(INSTALLED_BASE_DM);
327         }
328     }
329 
330     @Test
testInstallSomeApkIsMissingFsvSig_Base()331     public void testInstallSomeApkIsMissingFsvSig_Base()
332             throws DeviceNotAvailableException, FileNotFoundException {
333         new InstallMultiple()
334                 .addFileAndSignature(BASE_APK)
335                 .addFileAndSignature(BASE_APK_DM)
336                 .addFile(SPLIT_APK)
337                 .addFileAndSignature(SPLIT_APK_DM)
338                 .runExpectingFailure();
339     }
340 
341     @Test
testInstallSomeApkIsMissingFsvSig_Split()342     public void testInstallSomeApkIsMissingFsvSig_Split()
343             throws DeviceNotAvailableException, FileNotFoundException {
344         new InstallMultiple()
345                 .addFile(BASE_APK)
346                 .addFileAndSignature(BASE_APK_DM)
347                 .addFileAndSignature(SPLIT_APK)
348                 .addFileAndSignature(SPLIT_APK_DM)
349                 .runExpectingFailure();
350     }
351 
352     @Test
testInstallBaseWithFsvSigThenSplitWithout()353     public void testInstallBaseWithFsvSigThenSplitWithout()
354             throws DeviceNotAvailableException, FileNotFoundException {
355         new InstallMultiple()
356                 .addFileAndSignature(BASE_APK)
357                 .run();
358         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
359         verifyInstalledFiles(
360                 INSTALLED_BASE_APK,
361                 INSTALLED_BASE_APK_FSV_SIG);
362 
363         new InstallMultiple()
364                 .addFile(SPLIT_APK)
365                 .runExpectingFailure();
366     }
367 
368     @Test
testInstallBaseWithoutFsvSigThenSplitWith()369     public void testInstallBaseWithoutFsvSigThenSplitWith()
370             throws DeviceNotAvailableException, FileNotFoundException {
371         new InstallMultiple()
372                 .addFile(BASE_APK)
373                 .run();
374         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
375         verifyInstalledFiles(INSTALLED_BASE_APK);
376 
377         new InstallMultiple()
378                 .addFileAndSignature(SPLIT_APK)
379                 .runExpectingFailure();
380     }
381 
382     @Test
testFsverityFileIsImmutableAndReadable()383     public void testFsverityFileIsImmutableAndReadable() throws DeviceNotAvailableException {
384         new InstallMultiple().addFileAndSignature(BASE_APK).run();
385         String apkPath = getApkPath(TARGET_PACKAGE);
386 
387         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
388         expectRemoteCommandToFail("echo -n '' >> " + apkPath);
389         expectRemoteCommandToSucceed("cat " + apkPath + " > /dev/null");
390     }
391 
392     @Test
testFsverityFailToReadModifiedBlockAtFront()393     public void testFsverityFailToReadModifiedBlockAtFront() throws DeviceNotAvailableException {
394         new InstallMultiple().addFileAndSignature(BASE_APK).run();
395         String apkPath = getApkPath(TARGET_PACKAGE);
396 
397         long apkSize = getFileSizeInBytes(apkPath);
398         long offsetFirstByte = 0;
399 
400         // The first two pages should be both readable at first.
401         assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetFirstByte));
402         if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) {
403             assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath,
404                     offsetFirstByte + FSVERITY_PAGE_SIZE));
405         }
406 
407         // Damage the file directly against the block device.
408         damageFileAgainstBlockDevice(apkPath, offsetFirstByte);
409 
410         // Expect actual read from disk to fail but only at damaged page.
411         BlockDeviceWriter.dropCaches(mDevice);
412         assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetFirstByte));
413         if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) {
414             long lastByteOfTheSamePage =
415                     offsetFirstByte % FSVERITY_PAGE_SIZE + FSVERITY_PAGE_SIZE - 1;
416             assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, lastByteOfTheSamePage));
417             assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, lastByteOfTheSamePage + 1));
418         }
419     }
420 
421     @Test
testFsverityFailToReadModifiedBlockAtBack()422     public void testFsverityFailToReadModifiedBlockAtBack() throws DeviceNotAvailableException {
423         new InstallMultiple().addFileAndSignature(BASE_APK).run();
424         String apkPath = getApkPath(TARGET_PACKAGE);
425 
426         long apkSize = getFileSizeInBytes(apkPath);
427         long offsetOfLastByte = apkSize - 1;
428 
429         // The first two pages should be both readable at first.
430         assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetOfLastByte));
431         if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) {
432             assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath,
433                     offsetOfLastByte - FSVERITY_PAGE_SIZE));
434         }
435 
436         // Damage the file directly against the block device.
437         damageFileAgainstBlockDevice(apkPath, offsetOfLastByte);
438 
439         // Expect actual read from disk to fail but only at damaged page.
440         BlockDeviceWriter.dropCaches(mDevice);
441         assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetOfLastByte));
442         if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) {
443             long firstByteOfTheSamePage = offsetOfLastByte - offsetOfLastByte % FSVERITY_PAGE_SIZE;
444             assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, firstByteOfTheSamePage));
445             assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, firstByteOfTheSamePage - 1));
446         }
447     }
448 
verifyInstalledFilesHaveFsverity(String... filenames)449     private void verifyInstalledFilesHaveFsverity(String... filenames)
450             throws DeviceNotAvailableException {
451         // Verify that all files are protected by fs-verity
452         String apkPath = getApkPath(TARGET_PACKAGE);
453         String appDir = apkPath.substring(0, apkPath.lastIndexOf("/"));
454         long kTargetOffset = 0;
455         for (String basename : filenames) {
456             String path = appDir + "/" + basename;
457             damageFileAgainstBlockDevice(path, kTargetOffset);
458 
459             // Retry is sometimes needed to pass the test. Package manager may have FD leaks
460             // (see b/122744005 as example) that prevents the file in question to be evicted
461             // from filesystem cache. Forcing GC workarounds the problem.
462             int retry = 5;
463             for (; retry > 0; retry--) {
464                 BlockDeviceWriter.dropCaches(mDevice);
465                 if (!BlockDeviceWriter.canReadByte(mDevice, path, kTargetOffset)) {
466                     break;
467                 }
468                 try {
469                     String openFiles = expectRemoteCommandToSucceed("lsof " + apkPath);
470                     CLog.d("lsof: " + openFiles);
471                     Thread.sleep(1000);
472                     forceGCOnOpenFilesProcess(getOpenFilesPIDs(openFiles));
473                 } catch (InterruptedException e) {
474                     Thread.currentThread().interrupt();
475                     return;
476                 }
477             }
478             assertTrue("Read from " + path + " should fail", retry > 0);
479         }
480     }
481 
482     /**
483      * This is a helper method that parses the lsof output to get PIDs of process holding FD.
484      * Here is an example output of lsof. This method extracts the second columns(PID).
485      *
486      * Example lsof output:
487      *  COMMAND      PID     USER    FD     TYPE   DEVICE   SIZE/OFF  NODE   NAME
488      * .example.app  1063    u0_a38  mem    REG    253,6    8599      12826  example.apk
489      * .example.app  1063    u0_a38  99r    REG    253,6    8599      12826  example.apk
490      */
getOpenFilesPIDs(String lsof)491     private Set<String> getOpenFilesPIDs(String lsof) {
492         Set<String> openFilesPIDs = new HashSet<>();
493         String[] lines = lsof.split("\n");
494         for (int i = 1; i < lines.length; i++) {
495             openFilesPIDs.add(lines[i].split("\\s+")[1]);
496         }
497         return openFilesPIDs;
498     }
499 
500     /**
501      * This is a helper method that forces GC on processes given their PIDs.
502      * That is to execute shell command "kill -10" on PIDs.
503      */
forceGCOnOpenFilesProcess(Set<String> openFilesPIDs)504     private void forceGCOnOpenFilesProcess(Set<String> openFilesPIDs)
505             throws DeviceNotAvailableException {
506         for (String openFilePID : openFilesPIDs) {
507             mDevice.executeShellV2Command("kill -10 " + openFilePID);
508         }
509     }
510 
verifyInstalledFiles(String... filenames)511     private void verifyInstalledFiles(String... filenames) throws DeviceNotAvailableException {
512         String apkPath = getApkPath(TARGET_PACKAGE);
513         String appDir = apkPath.substring(0, apkPath.lastIndexOf("/"));
514         // Exclude directories since we only care about files.
515         HashSet<String> actualFiles = new HashSet<>(Arrays.asList(
516                 expectRemoteCommandToSucceed("ls -p " + appDir + " | grep -v '/'").split("\n")));
517 
518         HashSet<String> expectedFiles = new HashSet<>(Arrays.asList(filenames));
519         assertEquals(expectedFiles, actualFiles);
520     }
521 
damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte)522     private void damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte)
523             throws DeviceNotAvailableException {
524         assertTrue(path.startsWith("/data/"));
525         ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data");
526         ArrayList<String> args = new ArrayList<>();
527         args.add(DAMAGING_EXECUTABLE);
528         if ("f2fs".equals(mountPoint.type)) {
529             args.add("--use-f2fs-pinning");
530         }
531         args.add(mountPoint.filesystem);
532         args.add(path);
533         args.add(Long.toString(offsetOfTargetingByte));
534         expectRemoteCommandToSucceed(String.join(" ", args));
535     }
536 
getApkPath(String packageName)537     private String getApkPath(String packageName) throws DeviceNotAvailableException {
538         String line = expectRemoteCommandToSucceed("pm path " + packageName + " | grep base.apk");
539         int index = line.trim().indexOf(":");
540         assertTrue(index >= 0);
541         return line.substring(index + 1);
542     }
543 
getFileSizeInBytes(String packageName)544     private long getFileSizeInBytes(String packageName) throws DeviceNotAvailableException {
545         return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + packageName).trim());
546     }
547 
expectRemoteCommandToSucceed(String cmd)548     private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException {
549         CommandResult result = mDevice.executeShellV2Command(cmd);
550         assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS,
551                 result.getStatus());
552         return result.getStdout();
553     }
554 
expectRemoteCommandToFail(String cmd)555     private void expectRemoteCommandToFail(String cmd) throws DeviceNotAvailableException {
556         CommandResult result = mDevice.executeShellV2Command(cmd);
557         assertTrue("Unexpected success from `" + cmd + "`: " + result.getStderr(),
558                 result.getStatus() != CommandStatus.SUCCESS);
559     }
560 
561     private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> {
InstallMultiple()562         InstallMultiple() {
563             super(getDevice(), getBuild());
564         }
565 
addFileAndSignature(String filename)566         InstallMultiple addFileAndSignature(String filename) {
567             try {
568                 addFile(filename);
569                 addFile(filename + ".fsv_sig");
570             } catch (FileNotFoundException e) {
571                 fail("Missing test file: " + e);
572             }
573             return this;
574         }
575     }
576 }
577