1 /* 2 * Copyright (C) 2022 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.sts.common; 18 19 import static org.junit.Assert.assertTrue; 20 import static org.junit.Assert.assertNotNull; 21 import static com.android.sts.common.CommandUtil.runAndCheck; 22 23 import java.io.IOException; 24 import java.nio.charset.StandardCharsets; 25 import java.nio.file.Path; 26 import java.nio.file.Paths; 27 import java.util.ArrayList; 28 import java.util.HashMap; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 import org.junit.rules.TestWatcher; 34 import org.junit.runner.Description; 35 36 import com.android.tradefed.device.DeviceNotAvailableException; 37 import com.android.tradefed.device.ITestDevice; 38 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 39 import com.android.tradefed.util.CommandResult; 40 import com.android.tradefed.util.CommandStatus; 41 import com.google.common.hash.Hashing; 42 43 44 /** TestWatcher that enables writing to read-only partitions and reboots device when done. */ 45 public class OverlayFsUtils extends TestWatcher { 46 private static final String OVERLAYFS_PREFIX = "sts_overlayfs_"; 47 private static final Path WRITABLE_DIR = Paths.get("/data", "local", "tmp"); 48 49 private final BaseHostJUnit4Test test; 50 51 // output of `stat`, e.g. "root shell 755 u:object_r:vendor_file:s0" 52 static final Pattern PERM_PATTERN = 53 Pattern.compile( 54 "^(?<user>[a-zA-Z0-9_-]+) (?<group>[a-zA-Z0-9_-]+) (?<perm>[0-7]+)" 55 + " (?<secontext>.*)$"); 56 57 private Map<ITestDevice, List<String>> workingDirs = new HashMap<>(); 58 OverlayFsUtils(BaseHostJUnit4Test test)59 public OverlayFsUtils(BaseHostJUnit4Test test) { 60 assertNotNull("Need to pass in a valid testcase object.", test); 61 this.test = test; 62 } 63 64 /** 65 * Mounts an OverlayFS dir over the top most common dir in the list. 66 * 67 * <p>The directory should be writable after this returns successfully. To cleanup, reboot the 68 * device as unfortunately unmounting overlayfs is complicated. 69 * 70 * @param dir The directory to make writable. Directories with single quotes are not supported. 71 */ makeWritable(final String dir, int megabytes)72 public void makeWritable(final String dir, int megabytes) 73 throws DeviceNotAvailableException, IOException, IllegalStateException { 74 ITestDevice device = test.getDevice(); 75 assertNotNull("device not set.", device); 76 assertTrue("dir needs to be an absolute path.", dir.startsWith("/")); 77 78 // Setup list of temp dirs to be deleted upon cleanup 79 List<String> dirs; 80 if (!workingDirs.containsKey(device)) { 81 dirs = new ArrayList<>(2); 82 workingDirs.put(device, dirs); 83 } else { 84 dirs = workingDirs.get(device); 85 } 86 87 // losetup doesn't work for image paths 64 bytes or longer, so we have to truncate 88 String dirHash = Hashing.md5().hashString(dir, StandardCharsets.UTF_8).toString(); 89 int pathPrefixLength = WRITABLE_DIR.toString().length() + 1 + OVERLAYFS_PREFIX.length(); 90 int dirHashLength = Math.min(64 - pathPrefixLength - 5, dirHash.length()); 91 assertTrue("Can't fit overlayFS image path in 64 chars.", dirHashLength >= 5); 92 String id = OVERLAYFS_PREFIX + dirHash.substring(0, dirHashLength); 93 94 // Check and make sure we have not already mounted over this dir. We do that by hashing 95 // the lower dir path and put that as part of the device ID for `mount`. 96 CommandResult res = device.executeShellV2Command("mount | grep -q " + id); 97 if (res.getStatus() == CommandStatus.SUCCESS) { 98 // a mount with the same ID already exists 99 throw new IllegalStateException(dir + " has already been made writable."); 100 } 101 102 assertTrue("Can't acquire root for " + device.getSerialNumber(), device.enableAdbRoot()); 103 104 // Match permissions of upper dir to lower dir 105 String statOut = runAndCheck(device, "stat -c '%U %G %a %C' '" + dir + "'").getStdout(); 106 Matcher m = PERM_PATTERN.matcher(statOut); 107 assertTrue("Bad stats output: " + statOut, m.find()); 108 String user = m.group("user"); 109 String group = m.group("group"); 110 String unixPerm = m.group("perm"); 111 String seContext = m.group("secontext"); 112 113 // Disable SELinux enforcement and mount a loopback ext4 image 114 runAndCheck(device, "setenforce 0"); 115 Path tempdir = WRITABLE_DIR.resolve(id); 116 Path tempimg = tempdir.getParent().resolve(tempdir.getFileName().toString() + ".img"); 117 dirs.add(tempimg.toString()); 118 dirs.add(tempdir.toString()); 119 120 runAndCheck( 121 device, 122 String.format("dd if=/dev/zero of='%s' bs=%dM count=1", tempimg, megabytes)); 123 runAndCheck(device, String.format("mkfs.ext4 '%s'", tempimg)); 124 runAndCheck(device, String.format("mkdir '%s'", tempdir)); 125 126 String loopbackDev = 127 runAndCheck(device, String.format("losetup -f -s '%s'", tempimg), 3) 128 .getStdout() 129 .strip(); 130 runAndCheck(device, String.format("mount '%s' '%s'", loopbackDev, tempdir), 3); 131 132 String upperdir = tempdir.resolve("upper").toString(); 133 String workdir = tempdir.resolve("workdir").toString(); 134 135 runAndCheck(device, String.format("mkdir -p '%s' '%s'", upperdir, workdir)); 136 runAndCheck(device, String.format("chown %s:%s '%s'", user, group, upperdir)); 137 runAndCheck(device, String.format("chcon '%s' '%s'", seContext, upperdir)); 138 runAndCheck(device, String.format("chmod %s '%s'", unixPerm, upperdir)); 139 140 String mountCmd = 141 String.format( 142 "mount -t overlay '%s' -o lowerdir='%s',upperdir='%s',workdir='%s' '%s'", 143 id, dir, upperdir, workdir, dir); 144 runAndCheck(device, mountCmd); 145 } 146 anyOverlayFsMounted()147 public boolean anyOverlayFsMounted() throws DeviceNotAvailableException { 148 ITestDevice device = test.getDevice(); 149 assertNotNull("Device not set", device); 150 CommandResult res = device.executeShellV2Command("mount | grep -q " + OVERLAYFS_PREFIX); 151 return res.getStatus() == CommandStatus.SUCCESS; 152 } 153 154 @Override finished(Description d)155 public void finished(Description d) { 156 ITestDevice device = test.getDevice(); 157 assertNotNull("Device not set", device); 158 try { 159 // Since we can't umount an overlayfs cleanly, reboot the device to cleanup 160 if (anyOverlayFsMounted()) { 161 device.rebootUntilOnline(); 162 device.waitForDeviceAvailable(); 163 } 164 165 // Remove upper and working dirs 166 assertTrue("Can't acquire root: " + device.getSerialNumber(), device.enableAdbRoot()); 167 if (workingDirs.containsKey(device)) { 168 for (String dir : workingDirs.get(device)) { 169 runAndCheck(device, String.format("rm -rf '%s'", dir)); 170 } 171 } 172 173 // Restore SELinux enforcement state 174 runAndCheck(device, "setenforce 1"); 175 176 assertTrue("Can't remove root: " + device.getSerialNumber(), device.disableAdbRoot()); 177 } catch (DeviceNotAvailableException e) { 178 throw new AssertionError("Device unavailable when cleaning up", e); 179 } 180 } 181 } 182