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.tests.apex; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import static org.junit.Assume.assumeFalse; 23 import static org.junit.Assume.assumeTrue; 24 25 import android.cts.install.lib.host.InstallUtilsHost; 26 27 import com.android.tests.rollback.host.AbandonSessionsRule; 28 import com.android.tradefed.device.DeviceNotAvailableException; 29 import com.android.tradefed.device.ITestDevice; 30 import com.android.tradefed.device.ITestDevice.ApexInfo; 31 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 32 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 33 34 import org.junit.After; 35 import org.junit.Before; 36 import org.junit.Rule; 37 import org.junit.Test; 38 import org.junit.runner.RunWith; 39 40 import java.io.File; 41 import java.time.Duration; 42 import java.util.Set; 43 44 /** 45 * Test for automatic recovery of apex update that causes boot loop. 46 */ 47 @RunWith(DeviceJUnit4ClassRunner.class) 48 public class ApexRollbackTests extends BaseHostJUnit4Test { 49 private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); 50 @Rule 51 public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this); 52 53 private boolean mWasAdbRoot = false; 54 55 @Before setUp()56 public void setUp() throws Exception { 57 mHostUtils.uninstallShimApexIfNecessary(); 58 resetProperties(); 59 mWasAdbRoot = getDevice().isAdbRoot(); 60 if (!mWasAdbRoot) { 61 assumeTrue("Requires root", getDevice().enableAdbRoot()); 62 } 63 } 64 65 /** 66 * Uninstalls any version greater than 1 of shim apex and reboots the device if necessary 67 * to complete the uninstall. 68 */ 69 @After tearDown()70 public void tearDown() throws Exception { 71 mHostUtils.uninstallShimApexIfNecessary(); 72 resetProperties(); 73 if (!mWasAdbRoot) { 74 getDevice().disableAdbRoot(); 75 } 76 } 77 resetProperties()78 private void resetProperties() throws Exception { 79 resetProperty("persist.debug.trigger_watchdog.apex"); 80 resetProperty("persist.debug.trigger_updatable_crashing_for_testing"); 81 resetProperty("persist.debug.trigger_reboot_after_activation"); 82 resetProperty("persist.debug.trigger_reboot_twice_after_activation"); 83 } 84 resetProperty(String propertyName)85 private void resetProperty(String propertyName) throws Exception { 86 assertWithMessage("Failed to reset value of property %s", propertyName).that( 87 getDevice().setProperty(propertyName, "")).isTrue(); 88 } 89 90 /** 91 * Test for automatic recovery of apex update that causes boot loop. 92 */ 93 @Test testAutomaticBootLoopRecovery()94 public void testAutomaticBootLoopRecovery() throws Exception { 95 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 96 ITestDevice device = getDevice(); 97 // Skip this test if there is already crashing process on device 98 boolean hasCrashingProcess = 99 device.getBooleanProperty("sys.init.updatable_crashing", false); 100 String crashingProcess = device.getProperty("sys.init.updatable_crashing_process_name"); 101 assumeFalse( 102 "Device already has a crashing process: " + crashingProcess, hasCrashingProcess); 103 File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); 104 105 // To simulate an apex update that causes a boot loop, we install a 106 // trigger_watchdog.rc file that arranges for a trigger_watchdog.sh 107 // script to be run at boot. The trigger_watchdog.sh script checks if 108 // the apex version specified in the property 109 // persist.debug.trigger_watchdog.apex is installed. If so, 110 // trigger_watchdog.sh repeatedly kills the system server causing a 111 // boot loop. 112 assertThat(device.setProperty("persist.debug.trigger_watchdog.apex", 113 "com.android.apex.cts.shim@2")).isTrue(); 114 String error = mHostUtils.installStagedPackage(apexFile); 115 assertThat(error).isNull(); 116 117 String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions --only-ready " 118 + "--only-parent --only-sessionid").trim(); 119 assertThat(sessionIdToCheck).isNotEmpty(); 120 121 // After we reboot the device, we expect the device to go into boot 122 // loop from trigger_watchdog.sh. Native watchdog should detect and 123 // report the boot loop, causing apexd to roll back to the previous 124 // version of the apex and force reboot. When the device comes up 125 // after the forced reboot, trigger_watchdog.sh will see the different 126 // version of the apex and refrain from forcing a boot loop, so the 127 // device will be recovered. 128 device.reboot(); 129 130 ApexInfo ctsShimV1 = new ApexInfo("com.android.apex.cts.shim", 1L); 131 ApexInfo ctsShimV2 = new ApexInfo("com.android.apex.cts.shim", 2L); 132 Set<ApexInfo> activatedApexes = device.getActiveApexes(); 133 assertThat(activatedApexes).contains(ctsShimV1); 134 assertThat(activatedApexes).doesNotContain(ctsShimV2); 135 136 // Assert that a session has failed with the expected reason 137 String sessionInfo = device.executeShellCommand("cmd -w apexservice getStagedSessionInfo " 138 + sessionIdToCheck); 139 assertThat(sessionInfo).contains("revertReason: zygote"); 140 } 141 142 /** 143 * Test to verify that a device that does not support checkpointing will not revert a session 144 * if it reboots during boot. 145 */ 146 @Test testSessionNotRevertedWithCheckpointingDisabled()147 public void testSessionNotRevertedWithCheckpointingDisabled() throws Exception { 148 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 149 assumeFalse("Fs checkpointing is enabled", mHostUtils.isCheckpointSupported()); 150 151 File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); 152 153 ITestDevice device = getDevice(); 154 assertThat(device.setProperty("persist.debug.trigger_reboot_after_activation", 155 "com.android.apex.cts.shim@2.apex")).isTrue(); 156 assertThat(device.setProperty("debug.trigger_reboot_once_after_activation", 157 "1")).isTrue(); 158 159 String error = mHostUtils.installStagedPackage(apexFile); 160 assertThat(error).isNull(); 161 162 String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions --only-ready " 163 + "--only-parent --only-sessionid").trim(); 164 assertThat(sessionIdToCheck).isNotEmpty(); 165 166 // After we reboot the device, the apexd session should be activated as normal. After this, 167 // trigger_reboot.sh will reboot the device before the system server boots. 168 device.reboot(); 169 170 ApexInfo ctsShimV1 = new ApexInfo("com.android.apex.cts.shim", 1L); 171 ApexInfo ctsShimV2 = new ApexInfo("com.android.apex.cts.shim", 2L); 172 String stagedSessionInfo = getStagedSession(sessionIdToCheck); 173 assertThat(stagedSessionInfo).contains("isApplied = true"); 174 175 Set<ApexInfo> activatedApexes = device.getActiveApexes(); 176 assertThat(activatedApexes).contains(ctsShimV2); 177 assertThat(activatedApexes).doesNotContain(ctsShimV1); 178 } 179 180 /** 181 * Test to verify that rebooting twice when a session is activated will cause the session to 182 * be reverted due to filesystem checkpointing. 183 */ 184 @Test testCheckpointingRevertsSession()185 public void testCheckpointingRevertsSession() throws Exception { 186 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 187 assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); 188 189 File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); 190 191 ITestDevice device = getDevice(); 192 assertThat(device.setProperty("persist.debug.trigger_reboot_after_activation", 193 "com.android.apex.cts.shim@2.apex")).isTrue(); 194 assertThat(device.setProperty("persist.debug.trigger_reboot_twice_after_activation", 195 "1")).isTrue(); 196 String error = mHostUtils.installStagedPackage(apexFile); 197 assertThat(error).isNull(); 198 199 String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions --only-ready " 200 + "--only-parent --only-sessionid").trim(); 201 assertThat(sessionIdToCheck).isNotEmpty(); 202 203 // After we reboot the device, the apexd session should be activated as normal. After this, 204 // trigger_reboot.sh will reboot the device before the system server boots. Checkpointing 205 // will kick in, and at the next boot any non-finalized sessions will be reverted. 206 device.reboot(); 207 208 ApexInfo ctsShimV1 = new ApexInfo("com.android.apex.cts.shim", 1L); 209 ApexInfo ctsShimV2 = new ApexInfo("com.android.apex.cts.shim", 2L); 210 String stagedSessionInfo = getStagedSession(sessionIdToCheck); 211 assertThat(stagedSessionInfo).contains("isFailed = true"); 212 213 Set<ApexInfo> activatedApexes = device.getActiveApexes(); 214 assertThat(activatedApexes).contains(ctsShimV1); 215 assertThat(activatedApexes).doesNotContain(ctsShimV2); 216 } 217 218 /** 219 * Test to verify that rebooting once upon apex activation does not cause checkpointing to kick 220 * in and revert a session, since the checkpointing retry count should be 2. 221 */ 222 @Test testRebootingOnceDoesNotRevertSession()223 public void testRebootingOnceDoesNotRevertSession() throws Exception { 224 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 225 assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); 226 227 File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); 228 229 ITestDevice device = getDevice(); 230 assertThat(device.setProperty("persist.debug.trigger_reboot_after_activation", 231 "com.android.apex.cts.shim@2.apex")).isTrue(); 232 assertThat(device.setProperty("debug.trigger_reboot_once_after_activation", 233 "1")).isTrue(); 234 String error = mHostUtils.installStagedPackage(apexFile); 235 assertThat(error).isNull(); 236 237 String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions --only-ready " 238 + "--only-parent --only-sessionid").trim(); 239 assertThat(sessionIdToCheck).isNotEmpty(); 240 241 // After we reboot the device, the apexd session should be activated as normal. After this, 242 // trigger_reboot.sh will reboot the device before the system server boots. Checkpointing 243 // will kick in, and at the next boot any non-finalized sessions will be reverted. 244 device.reboot(); 245 246 ApexInfo ctsShimV1 = new ApexInfo("com.android.apex.cts.shim", 1L); 247 ApexInfo ctsShimV2 = new ApexInfo("com.android.apex.cts.shim", 2L); 248 String stagedSessionInfo = getStagedSession(sessionIdToCheck); 249 assertThat(stagedSessionInfo).contains("isApplied = true"); 250 251 Set<ApexInfo> activatedApexes = device.getActiveApexes(); 252 assertThat(activatedApexes).contains(ctsShimV2); 253 assertThat(activatedApexes).doesNotContain(ctsShimV1); 254 } 255 256 // TODO(ioffe): check that we recover from the boot loop in case of userspace reboot. 257 258 /** 259 * Test to verify that apexd won't boot loop a device in case {@code sys.init 260 * .updatable_crashing} is {@code true} and there is no apex session to revert. 261 */ 262 @Test testApexdDoesNotBootLoopDeviceIfThereIsNothingToRevert()263 public void testApexdDoesNotBootLoopDeviceIfThereIsNothingToRevert() throws Exception { 264 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 265 // On next boot trigger setprop sys.init.updatable_crashing 1, which will trigger a 266 // revert mechanism in apexd. Since there is nothing to revert, this should be a no-op 267 // and device will boot successfully. 268 assertThat(getDevice().setProperty("persist.debug.trigger_updatable_crashing_for_testing", 269 "1")).isTrue(); 270 getDevice().reboot(); 271 assertWithMessage("Device didn't boot in 1 minute").that( 272 getDevice().waitForBootComplete(Duration.ofMinutes(1).toMillis())).isTrue(); 273 // Verify that property was set to true. 274 assertThat(getDevice().getBooleanProperty("sys.init.updatable_crashing", false)).isTrue(); 275 } 276 277 /** 278 * Test to verify that if a hard reboot is triggered during userspace reboot boot 279 * sequence, an apex update will not be reverted. 280 */ 281 @Test testFailingUserspaceReboot_doesNotRevertUpdate()282 public void testFailingUserspaceReboot_doesNotRevertUpdate() throws Exception { 283 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 284 assumeTrue("Device doesn't support userspace reboot", 285 getDevice().getBooleanProperty("init.userspace_reboot.is_supported", false)); 286 assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); 287 288 File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); 289 // Simulate failure in userspace reboot by triggering a full reboot in the middle of the 290 // boot sequence. 291 assertThat(getDevice().setProperty("test.apex_revert_test_force_reboot", "1")).isTrue(); 292 String error = mHostUtils.installStagedPackage(apexFile); 293 assertWithMessage("Failed to stage com.android.apex.cts.shim.v2.apex : %s", error).that( 294 error).isNull(); 295 // After we reboot the device, apexd will apply the update 296 getDevice().rebootUserspace(); 297 // Verify that hard reboot happened. 298 assertThat(getDevice().getIntProperty("sys.init.userspace_reboot.last_finished", 299 -1)).isEqualTo(-1); 300 Set<ApexInfo> activatedApexes = getDevice().getActiveApexes(); 301 assertThat(activatedApexes).doesNotContain(new ApexInfo("com.android.apex.cts.shim", 1L)); 302 assertThat(activatedApexes).contains(new ApexInfo("com.android.apex.cts.shim", 2L)); 303 } 304 305 /** 306 * Test to verify that if a hard reboot is triggered before executing init executes {@code 307 * /system/bin/vdc checkpoint markBootAttempt} of userspace reboot boot sequence, apex update 308 * still will be installed. 309 */ 310 @Test testUserspaceRebootFailedShutdownSequence_doesNotRevertUpdate()311 public void testUserspaceRebootFailedShutdownSequence_doesNotRevertUpdate() throws Exception { 312 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 313 assumeTrue("Device doesn't support userspace reboot", 314 getDevice().getBooleanProperty("init.userspace_reboot.is_supported", false)); 315 assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); 316 317 File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); 318 // Simulate failure in userspace reboot by triggering a full reboot in the middle of the 319 // boot sequence. 320 assertThat(getDevice().setProperty("test.apex_userspace_reboot_simulate_shutdown_failed", 321 "1")).isTrue(); 322 String error = mHostUtils.installStagedPackage(apexFile); 323 assertWithMessage("Failed to stage com.android.apex.cts.shim.v2.apex : %s", error).that( 324 error).isNull(); 325 // After the userspace reboot started, we simulate it's failure by rebooting device during 326 // on userspace-reboot-requested action. Since boot attempt hasn't been marked yet, next 327 // boot will apply the update. 328 assertThat(getDevice().getIntProperty("test.apex_userspace_reboot_simulate_shutdown_failed", 329 0)).isEqualTo(1); 330 getDevice().rebootUserspace(); 331 // Verify that hard reboot happened. 332 assertThat(getDevice().getIntProperty("sys.init.userspace_reboot.last_finished", 333 -1)).isEqualTo(-1); 334 Set<ApexInfo> activatedApexes = getDevice().getActiveApexes(); 335 assertThat(activatedApexes).contains(new ApexInfo("com.android.apex.cts.shim", 2L)); 336 } 337 338 /** 339 * Test to verify that if a hard reboot is triggered around the time of 340 * executing {@code /system/bin/vdc checkpoint markBootAttempt} of userspace reboot boot 341 * sequence, apex update will still be installed. 342 */ 343 @Test testUserspaceRebootFailedRemount_revertsUpdate()344 public void testUserspaceRebootFailedRemount_revertsUpdate() throws Exception { 345 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 346 assumeTrue("Device doesn't support userspace reboot", 347 getDevice().getBooleanProperty("init.userspace_reboot.is_supported", false)); 348 assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); 349 350 File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); 351 // Simulate failure in userspace reboot by triggering a full reboot in the middle of the 352 // boot sequence. 353 assertThat(getDevice().setProperty("test.apex_userspace_reboot_simulate_remount_failed", 354 "1")).isTrue(); 355 String error = mHostUtils.installStagedPackage(apexFile); 356 assertWithMessage("Failed to stage com.android.apex.cts.shim.v2.apex : %s", error).that( 357 error).isNull(); 358 // After we reboot the device, apexd will apply the update 359 getDevice().rebootUserspace(); 360 // Verify that hard reboot happened. 361 assertThat(getDevice().getIntProperty("sys.init.userspace_reboot.last_finished", 362 -1)).isEqualTo(-1); 363 Set<ApexInfo> activatedApexes = getDevice().getActiveApexes(); 364 assertThat(activatedApexes).doesNotContain(new ApexInfo("com.android.apex.cts.shim", 1L)); 365 assertThat(activatedApexes).contains(new ApexInfo("com.android.apex.cts.shim", 2L)); 366 } 367 368 /** 369 * Test to verify that boot cleanup logic in apexd is triggered when there is a crash looping 370 * process, but there is nothing to revert. 371 */ 372 @Test testBootCompletedCleanupHappensEvenWhenThereIsCrashingProcess()373 public void testBootCompletedCleanupHappensEvenWhenThereIsCrashingProcess() throws Exception { 374 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 375 assumeTrue("Device requires root", getDevice().isAdbRoot()); 376 try { 377 // On next boot trigger setprop sys.init.updatable_crashing 1, which will trigger a 378 // revert mechanism in apexd. Since there is nothing to revert, this should be a no-op 379 // and device will boot successfully. 380 getDevice().setProperty("persist.debug.trigger_updatable_crashing_for_testing", "1"); 381 assertThat(getDevice().pushFile(mHostUtils.getTestFile("apex.apexd_test_v2.apex"), 382 "/data/apex/active/apexd_test_v2.apex")).isTrue(); 383 getDevice().reboot(); 384 assertWithMessage("Timed out waiting for device to boot").that( 385 getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); 386 // Verify that property was set to true. 387 assertThat( 388 getDevice().getBooleanProperty("sys.init.updatable_crashing", false)).isTrue(); 389 final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes(); 390 ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo( 391 "com.android.apex.cts.shim", 2L); 392 assertThat(activeApexes).doesNotContain(testApex); 393 mHostUtils.waitForFileDeleted("/data/apex/active/apexd_test_v2.apex", 394 Duration.ofMinutes(3)); 395 } finally { 396 getDevice().executeShellV2Command("rm /data/apex/active/apexd_test_v2.apex"); 397 } 398 } 399 400 /** 401 * Test reason for revert is properly logged during boot loops 402 */ 403 @Test testReasonForRevertIsLoggedDuringBootloop()404 public void testReasonForRevertIsLoggedDuringBootloop() throws Exception { 405 assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); 406 assumeTrue("Fs checkpointing is enabled", mHostUtils.isCheckpointSupported()); 407 408 ITestDevice device = getDevice(); 409 // Skip this test if there is already crashing process on device 410 final boolean hasCrashingProcess = 411 device.getBooleanProperty("sys.init.updatable_crashing", false); 412 final String crashingProcess = 413 device.getProperty("sys.init.updatable_crashing_process_name"); 414 assumeFalse( 415 "Device already has a crashing process: " + crashingProcess, hasCrashingProcess); 416 final File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); 417 418 // To simulate an apex update that causes a boot loop, we install a 419 // trigger_watchdog.rc file that arranges for a trigger_watchdog.sh 420 // script to be run at boot. The trigger_watchdog.sh script checks if 421 // the apex version specified in the property 422 // persist.debug.trigger_watchdog.apex is installed. If so, 423 // trigger_watchdog.sh repeatedly kills the system server causing a 424 // boot loop. 425 assertThat(device.setProperty("persist.debug.trigger_watchdog.apex", 426 "com.android.apex.cts.shim@2")).isTrue(); 427 final String error = mHostUtils.installStagedPackage(apexFile); 428 assertThat(error).isNull(); 429 430 final String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions " 431 + "--only-ready --only-parent --only-sessionid").trim(); 432 assertThat(sessionIdToCheck).isNotEmpty(); 433 434 // After we reboot the device, we expect the device to go into boot 435 // loop from trigger_watchdog.sh. Native watchdog should detect and 436 // report the boot loop, causing apexd to roll back to the previous 437 // version of the apex and force reboot. When the device comes up 438 // after the forced reboot, trigger_watchdog.sh will see the different 439 // version of the apex and refrain from forcing a boot loop, so the 440 // device will be recovered. 441 device.reboot(); 442 443 final ApexInfo ctsShimV1 = new ApexInfo("com.android.apex.cts.shim", 1L); 444 final ApexInfo ctsShimV2 = new ApexInfo("com.android.apex.cts.shim", 2L); 445 final Set<ApexInfo> activatedApexes = device.getActiveApexes(); 446 assertThat(activatedApexes).contains(ctsShimV1); 447 assertThat(activatedApexes).doesNotContain(ctsShimV2); 448 449 // Assert that a session has failed with the expected reason 450 final String stagedSessionString = getStagedSession(sessionIdToCheck); 451 assertThat(stagedSessionString).contains("Session reverted due to crashing native process"); 452 } 453 getStagedSession(String sessionId)454 String getStagedSession(String sessionId) throws DeviceNotAvailableException { 455 final String[] lines = getDevice().executeShellCommand( 456 "pm get-stagedsessions").split("\n"); 457 for (int i = 0; i < lines.length; i++) { 458 if (lines[i].startsWith("sessionId = " + sessionId + ";")) { 459 // Join all lines realted to this session 460 final StringBuilder result = new StringBuilder(lines[i]); 461 for (int j = i + 1; j < lines.length; j++) { 462 if (lines[j].startsWith("sessionId = ")) { 463 // A new session block has started 464 break; 465 } 466 result.append(lines[j]); 467 } 468 return result.toString(); 469 } 470 } 471 return ""; 472 } 473 } 474