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