diff options
author | 2024-03-28 18:33:17 +0000 | |
---|---|---|
committer | 2024-04-11 14:51:33 +0000 | |
commit | 69dc24557f951ce2513d0ea77f35a499fa58467b (patch) | |
tree | a3eb82a23fcab08399ba31696c60740620369c80 /dexopt_chroot_setup | |
parent | 53ca944020bb86199f6f80d8594d5deb1b1d46dd (diff) |
Implement dexopt_chroot_setup.
BYPASS_INCLUSIVE_LANGUAGE_REASON=preexisting terminology
Bug: 311377497
Test: atest art_standalone_dexopt_chroot_setup_tests
Change-Id: I2cbbd8c605d45b5451460306fc2291340668863b
Diffstat (limited to 'dexopt_chroot_setup')
-rw-r--r-- | dexopt_chroot_setup/Android.bp | 50 | ||||
-rw-r--r-- | dexopt_chroot_setup/README.md | 63 | ||||
-rw-r--r-- | dexopt_chroot_setup/art_standalone_dexopt_chroot_setup_tests.xml | 53 | ||||
-rw-r--r-- | dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl | 13 | ||||
-rw-r--r-- | dexopt_chroot_setup/dexopt_chroot_setup.cc | 449 | ||||
-rw-r--r-- | dexopt_chroot_setup/dexopt_chroot_setup.h | 18 | ||||
-rw-r--r-- | dexopt_chroot_setup/dexopt_chroot_setup_test.cc | 118 |
7 files changed, 734 insertions, 30 deletions
diff --git a/dexopt_chroot_setup/Android.bp b/dexopt_chroot_setup/Android.bp index 93e012a594..4f22fdf984 100644 --- a/dexopt_chroot_setup/Android.bp +++ b/dexopt_chroot_setup/Android.bp @@ -28,12 +28,16 @@ cc_defaults { srcs: [ "dexopt_chroot_setup.cc", ], + header_libs: [ + "libarttools_binder_utils", + ], shared_libs: [ "libbase", "libbinder_ndk", ], static_libs: [ "dexopt_chroot_setup-aidl-ndk", + "libfstab", ], } @@ -46,6 +50,7 @@ art_cc_binary { shared_libs: [ "libart", "libartbase", + "libarttools", // Contains "libc++fs". ], apex_available: [ "com.android.art", @@ -53,40 +58,27 @@ art_cc_binary { ], } -art_cc_defaults { - name: "art_dexopt_chroot_setup_tests_defaults", - defaults: ["dexopt_chroot_setup_defaults"], - static_libs: [ - "libgmock", +// This test only has the standalone version. A bundled test runs on host and in +// chroot, neither of which is suitable for this test because this test sets up +// a real chroot environment. In contrast, a standalone test runs on device by +// tradefed and atest locally. +art_cc_test { + name: "art_standalone_dexopt_chroot_setup_tests", + defaults: [ + "art_standalone_gtest_defaults", + "dexopt_chroot_setup_defaults", ], srcs: [ "dexopt_chroot_setup_test.cc", ], -} - -// Version of ART gtest `art_dexopt_chroot_setup_tests` bundled with the ART -// APEX on target. -// -// This test requires the full libbinder_ndk implementation on host, which is -// not available as a prebuilt on the thin master-art branch. Hence it won't -// work there, and there's a conditional in Android.gtest.mk to exclude it from -// test-art-host-gtest. -art_cc_test { - name: "art_dexopt_chroot_setup_tests", - defaults: [ - "art_gtest_defaults", - "art_dexopt_chroot_setup_tests_defaults", + data: [ + ":art-gtest-jars-Main", ], -} - -// Standalone version of ART gtest `art_dexopt_chroot_setup_tests`, not bundled -// with the ART APEX on target. -art_cc_test { - name: "art_standalone_dexopt_chroot_setup_tests", - defaults: [ - "art_standalone_gtest_defaults", - "art_dexopt_chroot_setup_tests_defaults", + static_libs: [ + "libarttools", // Contains "libc++fs". + "libgmock", ], + test_config_template: "art_standalone_dexopt_chroot_setup_tests.xml", } cc_fuzz { @@ -96,12 +88,12 @@ cc_fuzz { "art_module_source_build_defaults", "dexopt_chroot_setup_defaults", ], - host_supported: true, srcs: ["dexopt_chroot_setup_fuzzer.cc"], shared_libs: [ "libart", "libartbase", "liblog", + "libarttools", // Contains "libc++fs". ], fuzz_config: { cc: [ diff --git a/dexopt_chroot_setup/README.md b/dexopt_chroot_setup/README.md index 71401248d6..b94cde2307 100644 --- a/dexopt_chroot_setup/README.md +++ b/dexopt_chroot_setup/README.md @@ -8,3 +8,66 @@ seamless updates. It requires elevated permissions that are not available to system_server, such as mounting filesystems. It publishes a binder interface that is internal to ART Service's Java code. + +### Pre-reboot Dexopt file structure + +#### Overview + +``` +/mnt/pre_reboot_dexopt +|-- chroot +| |-- system_ext +| |-- vendor +| |-- product +| |-- data +| |-- mnt +| | |-- expand +| | `-- artd_tmp +| |-- dev +| |-- proc +| |-- sys +| |-- metadata +| |-- apex +| `-- linkerconfig +`-- mount_tmp +``` + +#### `/mnt/pre_reboot_dexopt` + +The root directory for Pre-reboot Dexopt, prepared by `init`. + +#### `/mnt/pre_reboot_dexopt/chroot` + +The root directory of the chroot environment for Pre-reboot Dexopt. It is the +mount point of the `system` image. Created by `dexopt_chroot_setup`, and only +exists for the duration of the Pre-reboot Dexopt. + +#### `/mnt/pre_reboot_dexopt/chroot/{system_ext,vendor,product}` + +Mount points of other readonly images. + +#### `/mnt/pre_reboot_dexopt/chroot/{data,mnt/expand,dev,proc,sys,metadata}` + +Same as the corresponding directories outside of chroot. These are read-write +mounts. + +#### `/mnt/pre_reboot_dexopt/chroot/mnt/artd_tmp` + +An empty directory for storing temporary files during Pre-reboot Dexopt, managed +by `artd`. + +#### `/mnt/pre_reboot_dexopt/chroot/apex` + +For holding the apex mount points used in the chroot environment, managed by +`apexd`. Note that this is not the same as `/apex` outside of chroot. + +#### `/mnt/pre_reboot_dexopt/chroot/linkerconfig` + +For holding the linker config used in the chroot environment, managed by +`linkerconfig`. Note that this is not the same as `/linkerconfig` outside of +chroot. + +#### `/mnt/pre_reboot_dexopt/mount_tmp` + +An ephemeral directory used as a temporary mount point for bind-mounting +directories "slave+shared". diff --git a/dexopt_chroot_setup/art_standalone_dexopt_chroot_setup_tests.xml b/dexopt_chroot_setup/art_standalone_dexopt_chroot_setup_tests.xml new file mode 100644 index 0000000000..8a0596c319 --- /dev/null +++ b/dexopt_chroot_setup/art_standalone_dexopt_chroot_setup_tests.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<!-- Note: This test config file for {MODULE} is generated from a template. --> +<configuration description="Runs {MODULE}."> + <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.art.apex" /> + <option name="config-descriptor:metadata" key="mainline-param" value="com.android.art.apex" /> + + <!-- This test sets up a real chroot environment, so it needs to have root access and bypass + SELinux checks.--> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> + <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer" /> + + <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> + <option name="cleanup" value="true" /> + <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}/{MODULE}" /> + <option name="append-bitness" value="true" /> + </target_preparer> + + <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> + <option name="cleanup" value="true" /> + <option name="push" value="art-gtest-jars-Main.jar->/data/local/tmp/{MODULE}/art-gtest-jars-Main.jar" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.GTest" > + <option name="native-test-device-path" value="/data/local/tmp/{MODULE}" /> + <option name="module-name" value="{MODULE}" /> + </test> + + <!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if + one of the Mainline modules below is present on the device used for testing. --> + <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <!-- ART Mainline Module (internal version). --> + <option name="mainline-module-package-name" value="com.google.android.art" /> + <!-- ART Mainline Module (external (AOSP) version). --> + <option name="mainline-module-package-name" value="com.android.art" /> + </object> + + <!-- Only run tests if the device under test is SDK version 34 (Android 14) or above. --> + <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" /> +</configuration> diff --git a/dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl b/dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl index f38fceade8..0b50d22778 100644 --- a/dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl +++ b/dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl @@ -18,4 +18,17 @@ package com.android.server.art; /** @hide */ interface IDexoptChrootSetup { + const @utf8InCpp String PRE_REBOOT_DEXOPT_DIR = "/mnt/pre_reboot_dexopt"; + const @utf8InCpp String CHROOT_DIR = PRE_REBOOT_DEXOPT_DIR + "/chroot"; + + /** + * Sets up the chroot environment. + * + * @param otaSlot The slot that contains the OTA update, "_a" or "_b", or null for a Mainline + * update. + */ + void setUp(@nullable @utf8InCpp String otaSlot); + + /** Tears down the chroot environment. */ + void tearDown(); } diff --git a/dexopt_chroot_setup/dexopt_chroot_setup.cc b/dexopt_chroot_setup/dexopt_chroot_setup.cc index c9a1c735c2..967cb20c1e 100644 --- a/dexopt_chroot_setup/dexopt_chroot_setup.cc +++ b/dexopt_chroot_setup/dexopt_chroot_setup.cc @@ -16,26 +16,315 @@ #include "dexopt_chroot_setup.h" +#include <linux/mount.h> +#include <sched.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <chrono> +#include <cstring> +#include <filesystem> +#include <mutex> +#include <optional> +#include <string> +#include <string_view> +#include <system_error> +#include <vector> + #include "aidl/com/android/server/art/BnDexoptChrootSetup.h" #include "android-base/errors.h" +#include "android-base/file.h" +#include "android-base/logging.h" +#include "android-base/no_destructor.h" +#include "android-base/properties.h" #include "android-base/result.h" +#include "android-base/strings.h" #include "android/binder_auto_utils.h" #include "android/binder_manager.h" #include "android/binder_process.h" +#include "base/file_utils.h" +#include "base/macros.h" +#include "base/os.h" +#include "exec_utils.h" +#include "fstab/fstab.h" +#include "tools/binder_utils.h" +#include "tools/cmdline_builder.h" +#include "tools/tools.h" namespace art { namespace dexopt_chroot_setup { namespace { +using ::android::base::ConsumePrefix; +using ::android::base::EndsWith; using ::android::base::Error; +using ::android::base::Join; +using ::android::base::NoDestructor; +using ::android::base::ReadFileToString; using ::android::base::Result; +using ::android::base::SetProperty; +using ::android::base::Split; +using ::android::base::StringReplace; +using ::android::base::Tokenize; +using ::android::base::WaitForProperty; +using ::android::fs_mgr::FstabEntry; +using ::art::tools::CmdlineBuilder; +using ::art::tools::Fatal; +using ::art::tools::GetProcMountsDescendantsOfPath; +using ::art::tools::NonFatal; +using ::art::tools::PathStartsWith; using ::ndk::ScopedAStatus; constexpr const char* kServiceName = "dexopt_chroot_setup"; +const NoDestructor<std::string> kBindMountTmpDir( + std::string(DexoptChrootSetup::PRE_REBOOT_DEXOPT_DIR) + "/mount_tmp"); +constexpr mode_t kChrootDefaultMode = 0755; +constexpr std::chrono::milliseconds kSnapshotCtlTimeout = std::chrono::seconds(60); + +bool IsOtaUpdate(const std::optional<std::string> ota_slot) { return ota_slot.has_value(); } + +Result<void> Run(std::string_view log_name, const std::vector<std::string>& args) { + LOG(INFO) << "Running " << log_name << ": " << Join(args, /*separator=*/" "); + + std::string error_msg; + if (!Exec(args, &error_msg)) { + return Errorf("Failed to run {}: {}", log_name, error_msg); + } + + LOG(INFO) << log_name << " returned code 0"; + return {}; +} + +Result<std::string> GetArtExec() { + std::string error_msg; + std::string art_root = GetArtRootSafe(&error_msg); + if (!error_msg.empty()) { + return Error() << error_msg; + } + return art_root + "/bin/art_exec"; +} + +Result<void> CreateDir(const std::string& path) { + std::error_code ec; + std::filesystem::create_directory(path, ec); + if (ec) { + return Errorf("Failed to create dir '{}': {}", path, ec.message()); + } + return {}; +} + +Result<void> BindMount(const std::string& source, const std::string& target) { + // Don't bind-mount repeatedly. + CHECK(!PathStartsWith(source, DexoptChrootSetup::CHROOT_DIR)); + // system_server has a different mount namespace from init, and it uses slave mounts. E.g: + // + // a: init mount ns: shared(1): /foo + // b: init mount ns: shared(2): /mnt + // c: SS mount ns: slave(1): /foo + // d: SS mount ns: slave(2): /mnt + // + // We create our chroot setup in the init namespace but also want it to appear inside the + // system_server one, since we need to access some files in it from system_server (in particular + // service-art.jar). + // + // Hence we want the mount propagation type to be "slave+shared": Slave of the init namespace so + // that unmounts in the chroot doesn't affect the rest of the system, while at the same time + // shared with the system_server namespace so that it gets the same mounts recursively in the + // chroot tree. This can be achieved in 4 steps: + // + // 1. Bind-mount /foo at a temp mount point /mnt/pre_reboot_dexopt/mount_tmp. + // a: init mount ns: shared(1): /foo + // b: init mount ns: shared(2): /mnt + // e: init mount ns: shared(1): /mnt/pre_reboot_dexopt/mount_tmp + // c: SS mount ns: slave(1): /foo + // d: SS mount ns: slave(2): /mnt + // f: SS mount ns: slave(1): /mnt/pre_reboot_dexopt/mount_tmp + // + // 2. Make the temp mount point slave. + // a: init mount ns: shared(1): /foo + // b: init mount ns: shared(2): /mnt + // e: init mount ns: slave(1): /mnt/pre_reboot_dexopt/mount_tmp + // c: SS mount ns: slave(1): /foo + // d: SS mount ns: slave(2): /mnt + // f: SS mount ns: slave(1): /mnt/pre_reboot_dexopt/mount_tmp + // + // 3. Bind-mount the temp mount point at /mnt/pre_reboot_dexopt/chroot/foo. (The new mount point + // gets "slave+shared". It gets "slave" because the source (`e`) is "slave", and it gets + // "shared" because the dest (`b`) is "shared".) + // a: init mount ns: shared(1): /foo + // b: init mount ns: shared(2): /mnt + // e: init mount ns: slave(1): /mnt/pre_reboot_dexopt/mount_tmp + // g: init mount ns: slave(1),shared(3): /mnt/pre_reboot_dexopt/chroot/foo + // b: SS mount ns: slave(1): /foo + // d: SS mount ns: slave(2): /mnt + // f: SS mount ns: slave(1): /mnt/pre_reboot_dexopt/mount_tmp + // h: SS mount ns: slave(3): /mnt/pre_reboot_dexopt/chroot/foo + // + // 4. Unmount the temp mount point. + // a: init mount ns: shared(1): /foo + // b: init mount ns: shared(2): /mnt + // g: init mount ns: slave(1),shared(3): /mnt/pre_reboot_dexopt/chroot/foo + // b: SS mount ns: slave(1): /foo + // d: SS mount ns: slave(2): /mnt + // h: SS mount ns: slave(3): /mnt/pre_reboot_dexopt/chroot/foo + // + // At this point, we have achieved what we want. `g` is a slave of `a` so that unmounts in `g` + // doesn't affect `a`, and `g` is shared with `h` so that mounts in `g` are propagated to `h`. + OR_RETURN(CreateDir(*kBindMountTmpDir)); + if (mount(source.c_str(), + kBindMountTmpDir->c_str(), + /*fs_type=*/nullptr, + MS_BIND, + /*data=*/nullptr) != 0) { + return ErrnoErrorf("Failed to bind-mount '{}' at '{}'", source, *kBindMountTmpDir); + } + if (mount(/*source=*/nullptr, + kBindMountTmpDir->c_str(), + /*fs_type=*/nullptr, + MS_SLAVE, + /*data=*/nullptr) != 0) { + return ErrnoErrorf("Failed to make mount slave for '{}'", *kBindMountTmpDir); + } + if (mount(kBindMountTmpDir->c_str(), + target.c_str(), + /*fs_type=*/nullptr, + MS_BIND, + /*data=*/nullptr) != 0) { + return ErrnoErrorf("Failed to bind-mount '{}' at '{}'", *kBindMountTmpDir, target); + } + if (umount2(kBindMountTmpDir->c_str(), UMOUNT_NOFOLLOW) != 0) { + return ErrnoErrorf("Failed to umount2 '{}'", *kBindMountTmpDir); + } + LOG(INFO) << ART_FORMAT("Bind-mounted '{}' at '{}'", source, target); + return {}; +} + +Result<void> BindMountRecursive(const std::string& source, const std::string& target) { + CHECK(!EndsWith(source, '/')); + OR_RETURN(BindMount(source, target)); + + // Mount and make slave one by one. Do not use MS_REC because we don't want to mount a child if + // the parent cannot be slave (i.e., is shared). Otherwise, unmount events will be undesirably + // propagated to the source. For example, if "/dev" and "/dev/pts" are mounted at "/chroot/dev" + // and "/chroot/dev/pts" respectively, and "/chroot/dev" is shared, then unmounting + // "/chroot/dev/pts" will also unmount "/dev/pts". + // + // The list is in mount order. + std::vector<FstabEntry> entries = OR_RETURN(GetProcMountsDescendantsOfPath(source)); + for (const FstabEntry& entry : entries) { + CHECK(!EndsWith(entry.mount_point, '/')); + std::string_view sub_dir = entry.mount_point; + CHECK(ConsumePrefix(&sub_dir, source)); + if (sub_dir.empty()) { + // `source` itself. Already mounted. + continue; + } + OR_RETURN(BindMount(entry.mount_point, std::string(target).append(sub_dir))); + } + return {}; +} + +std::string GetBlockDeviceName(const std::string& partition, const std::string& slot) { + return ART_FORMAT("/dev/block/mapper/{}{}", partition, slot); +} + +Result<std::vector<std::string>> GetSupportedFilesystems() { + std::string content; + if (!ReadFileToString("/proc/filesystems", &content)) { + return ErrnoErrorf("Failed to read '/proc/filesystems'"); + } + std::vector<std::string> filesystems; + for (const std::string& line : Split(content, "\n")) { + std::vector<std::string> tokens = Tokenize(line, " \t"); + // If there are two tokens, the first token is a "nodev" mark, meaning it's not for a block + // device, so we skip it. + if (tokens.size() == 1) { + filesystems.push_back(tokens[0]); + } + } + // Prioritize the filesystems that are known to behave correctly, just in case some bad + // filesystems are unexpectedly happy to mount volumes that aren't of their types. We have never + // seen this case in practice though. + constexpr const char* kWellKnownFilesystems[] = {"erofs", "ext4"}; + for (const char* well_known_fs : kWellKnownFilesystems) { + auto it = std::find(filesystems.begin(), filesystems.end(), well_known_fs); + if (it != filesystems.end()) { + filesystems.erase(it); + filesystems.insert(filesystems.begin(), well_known_fs); + } + } + return filesystems; +} + +Result<void> Mount(const std::string& block_device, const std::string& target) { + static const NoDestructor<Result<std::vector<std::string>>> supported_filesystems( + GetSupportedFilesystems()); + if (!supported_filesystems->ok()) { + return supported_filesystems->error(); + } + std::vector<std::string> error_msgs; + for (const std::string& filesystem : supported_filesystems->value()) { + if (mount(block_device.c_str(), + target.c_str(), + filesystem.c_str(), + MS_RDONLY, + /*data=*/nullptr) == 0) { + // Success. + LOG(INFO) << ART_FORMAT( + "Mounted '{}' at '{}' with type '{}'", block_device, target, filesystem); + return {}; + } else { + error_msgs.push_back(ART_FORMAT("Tried '{}': {}", filesystem, strerror(errno))); + if (errno != EINVAL && errno != EBUSY) { + // If the filesystem type is wrong, `errno` must be either `EINVAL` or `EBUSY`. For example, + // we've seen that trying to mount a device with a wrong filesystem type yields `EBUSY` if + // the device is also mounted elsewhere, though we can't find any document about this + // behavior. + break; + } + } + } + return Errorf("Failed to mount '{}' at '{}':\n{}", block_device, target, Join(error_msgs, '\n')); +} + +Result<void> MountTmpfs(const std::string& target, std::string_view se_context) { + if (mount(/*source=*/"tmpfs", + target.c_str(), + /*fs_type=*/"tmpfs", + MS_NODEV | MS_NOEXEC | MS_NOSUID, + ART_FORMAT("mode={:#o},rootcontext={}", kChrootDefaultMode, se_context).c_str()) != 0) { + return ErrnoErrorf("Failed to mount tmpfs at '{}'", target); + } + return {}; +} } // namespace +ScopedAStatus DexoptChrootSetup::setUp(const std::optional<std::string>& in_otaSlot) { + if (!mu_.try_lock()) { + return Fatal("Unexpected concurrent calls"); + } + std::lock_guard<std::mutex> lock(mu_, std::adopt_lock); + + if (in_otaSlot.has_value() && (in_otaSlot.value() != "_a" && in_otaSlot.value() != "_b")) { + return Fatal(ART_FORMAT("Invalid OTA slot '{}'", in_otaSlot.value())); + } + OR_RETURN_NON_FATAL(SetUpChroot(in_otaSlot)); + return ScopedAStatus::ok(); +} + +ScopedAStatus DexoptChrootSetup::tearDown() { + if (!mu_.try_lock()) { + return Fatal("Unexpected concurrent calls"); + } + std::lock_guard<std::mutex> lock(mu_, std::adopt_lock); + + OR_RETURN_NON_FATAL(TearDownChroot()); + return ScopedAStatus::ok(); +} + Result<void> DexoptChrootSetup::Start() { ScopedAStatus status = ScopedAStatus::fromStatus( AServiceManager_registerLazyService(this->asBinder().get(), kServiceName)); @@ -48,5 +337,165 @@ Result<void> DexoptChrootSetup::Start() { return {}; } +Result<void> DexoptChrootSetup::SetUpChroot(const std::optional<std::string>& ota_slot) const { + // Set the default permission mode for new files and dirs to be `kChrootDefaultMode`. + umask(~kChrootDefaultMode & 0777); + + // In case there is some leftover. + OR_RETURN(TearDownChroot()); + + // Prepare the root dir of chroot. The parent directory has been created by init (see `init.rc`). + OR_RETURN(CreateDir(CHROOT_DIR)); + LOG(INFO) << ART_FORMAT("Created '{}'", CHROOT_DIR); + + std::vector<std::string> additional_system_partitions = { + "system_ext", + "vendor", + "product", + }; + + if (!IsOtaUpdate(ota_slot)) { // Mainline update + OR_RETURN(BindMount("/", CHROOT_DIR)); + for (const std::string& partition : additional_system_partitions) { + OR_RETURN(BindMount("/" + partition, PathInChroot("/" + partition))); + } + } else { + CHECK(ota_slot.value() == "_a" || ota_slot.value() == "_b"); + + // Run `snapshotctl map` through init to map block devices. We can't run it ourselves because it + // requires the UID to be 0. See `sys.snapshotctl.map` in `init.rc`. + if (!SetProperty("sys.snapshotctl.map", "requested")) { + return Errorf("Failed to request snapshotctl map"); + } + if (!WaitForProperty("sys.snapshotctl.map", "finished", kSnapshotCtlTimeout)) { + return Errorf("snapshotctl timed out"); + } + + // We don't know whether snapshotctl succeeded or not, but if it failed, the mount operation + // below will fail with `ENOENT`. + OR_RETURN(Mount(GetBlockDeviceName("system", ota_slot.value()), CHROOT_DIR)); + for (const std::string& partition : additional_system_partitions) { + OR_RETURN( + Mount(GetBlockDeviceName(partition, ota_slot.value()), PathInChroot("/" + partition))); + } + } + + OR_RETURN(MountTmpfs(PathInChroot("/apex"), "u:object_r:apex_mnt_dir:s0")); + OR_RETURN(MountTmpfs(PathInChroot("/linkerconfig"), "u:object_r:linkerconfig_file:s0")); + OR_RETURN(MountTmpfs(PathInChroot("/mnt"), "u:object_r:pre_reboot_dexopt_file:s0")); + OR_RETURN(CreateDir(PathInChroot("/mnt/artd_tmp"))); + OR_RETURN(MountTmpfs(PathInChroot("/mnt/artd_tmp"), "u:object_r:pre_reboot_dexopt_artd_file:s0")); + OR_RETURN(CreateDir(PathInChroot("/mnt/expand"))); + + std::vector<std::string> bind_mount_srcs = { + // Data partitions. + "/data", + "/mnt/expand", + // Linux API filesystems. + "/dev", + "/proc", + "/sys", + // For apexd to query staged APEX sessions. + "/metadata", + }; + + for (const std::string& src : bind_mount_srcs) { + OR_RETURN(BindMountRecursive(src, PathInChroot(src))); + } + + // Generate empty linker config to suppress warnings. + if (!android::base::WriteStringToFile("", PathInChroot("/linkerconfig/ld.config.txt"))) { + PLOG(WARNING) << "Failed to generate empty linker config to suppress warnings"; + } + + CmdlineBuilder args; + args.Add(OR_RETURN(GetArtExec())) + .Add("--chroot=%s", CHROOT_DIR) + .Add("--") + .Add("/system/bin/apexd") + .Add("--otachroot-bootstrap") + .AddIf(!IsOtaUpdate(ota_slot), "--also-include-staged-apexes"); + OR_RETURN(Run("apexd", args.Get())); + + args = CmdlineBuilder(); + args.Add(OR_RETURN(GetArtExec())) + .Add("--chroot=%s", CHROOT_DIR) + .Add("--drop-capabilities") + .Add("--") + .Add("/apex/com.android.runtime/bin/linkerconfig") + .Add("--target") + .Add("/linkerconfig"); + OR_RETURN(Run("linkerconfig", args.Get())); + + return {}; +} + +Result<void> DexoptChrootSetup::TearDownChroot() const { + if (OS::FileExists(PathInChroot("/system/bin/apexd").c_str())) { + CmdlineBuilder args; + args.Add(OR_RETURN(GetArtExec())) + .Add("--chroot=%s", CHROOT_DIR) + .Add("--") + .Add("/system/bin/apexd") + .Add("--unmount-all") + .Add("--also-include-staged-apexes"); + if (Result<void> result = Run("apexd", args.Get()); !result.ok()) { + // Maybe apexd is not executable because a previous setup/teardown failed halfway (e.g., + // /system is currently mounted but /dev is not). We do a check below to see if there is any + // unmounted APEXes. + LOG(WARNING) << "Failed to run apexd: " << result.error().message(); + } + } + + std::vector<FstabEntry> apex_entries = + OR_RETURN(GetProcMountsDescendantsOfPath(PathInChroot("/apex"))); + for (const FstabEntry& entry : apex_entries) { + if (entry.mount_point != PathInChroot("/apex")) { + return Errorf("apexd didn't unmount '{}'. See logs for details", entry.mount_point); + } + } + + // The list is in mount order. + std::vector<FstabEntry> entries = OR_RETURN(GetProcMountsDescendantsOfPath(CHROOT_DIR)); + for (auto it = entries.rbegin(); it != entries.rend(); it++) { + if (umount2(it->mount_point.c_str(), UMOUNT_NOFOLLOW) != 0) { + return ErrnoErrorf("Failed to umount2 '{}'", it->mount_point); + } + LOG(INFO) << ART_FORMAT("Unmounted '{}'", it->mount_point); + } + + std::error_code ec; + std::uintmax_t removed = std::filesystem::remove_all(CHROOT_DIR, ec); + if (ec) { + return Errorf("Failed to remove dir '{}': {}", CHROOT_DIR, ec.message()); + } + if (removed > 0) { + LOG(INFO) << ART_FORMAT("Removed '{}'", CHROOT_DIR); + } + + if (!OR_RETURN(GetProcMountsDescendantsOfPath(*kBindMountTmpDir)).empty() && + umount2(kBindMountTmpDir->c_str(), UMOUNT_NOFOLLOW) != 0) { + return ErrnoErrorf("Failed to umount2 '{}'", *kBindMountTmpDir); + } + + std::filesystem::remove_all(*kBindMountTmpDir, ec); + if (ec) { + return Errorf("Failed to remove dir '{}': {}", *kBindMountTmpDir, ec.message()); + } + + if (!SetProperty("sys.snapshotctl.unmap", "requested")) { + return Errorf("Failed to request snapshotctl unmap"); + } + if (!WaitForProperty("sys.snapshotctl.unmap", "finished", kSnapshotCtlTimeout)) { + return Errorf("snapshotctl timed out"); + } + + return {}; +} + +std::string PathInChroot(std::string_view path) { + return std::string(DexoptChrootSetup::CHROOT_DIR).append(path); +} + } // namespace dexopt_chroot_setup } // namespace art diff --git a/dexopt_chroot_setup/dexopt_chroot_setup.h b/dexopt_chroot_setup/dexopt_chroot_setup.h index 858c71711d..baceb1a480 100644 --- a/dexopt_chroot_setup/dexopt_chroot_setup.h +++ b/dexopt_chroot_setup/dexopt_chroot_setup.h @@ -17,8 +17,12 @@ #ifndef ART_DEXOPT_CHROOT_SETUP_DEXOPT_CHROOT_SETUP_H_ #define ART_DEXOPT_CHROOT_SETUP_DEXOPT_CHROOT_SETUP_H_ +#include <optional> +#include <string> + #include "aidl/com/android/server/art/BnDexoptChrootSetup.h" #include "android-base/result.h" +#include "android-base/thread_annotations.h" namespace art { namespace dexopt_chroot_setup { @@ -26,9 +30,23 @@ namespace dexopt_chroot_setup { // A service that sets up the chroot environment for Pre-reboot Dexopt. class DexoptChrootSetup : public aidl::com::android::server::art::BnDexoptChrootSetup { public: + ndk::ScopedAStatus setUp(const std::optional<std::string>& in_otaSlot) override; + + ndk::ScopedAStatus tearDown() override; + android::base::Result<void> Start(); + + private: + android::base::Result<void> SetUpChroot(const std::optional<std::string>& ota_slot) const + REQUIRES(mu_); + + android::base::Result<void> TearDownChroot() const REQUIRES(mu_); + + std::mutex mu_; }; +std::string PathInChroot(std::string_view path); + } // namespace dexopt_chroot_setup } // namespace art diff --git a/dexopt_chroot_setup/dexopt_chroot_setup_test.cc b/dexopt_chroot_setup/dexopt_chroot_setup_test.cc index ba42fc53ea..c3896e6ce0 100644 --- a/dexopt_chroot_setup/dexopt_chroot_setup_test.cc +++ b/dexopt_chroot_setup/dexopt_chroot_setup_test.cc @@ -16,25 +16,141 @@ #include "dexopt_chroot_setup.h" +#include <unistd.h> + +#include <filesystem> +#include <string> + +#include "aidl/com/android/server/art/BnDexoptChrootSetup.h" #include "base/common_art_test.h" +#include "exec_utils.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "tools/cmdline_builder.h" namespace art { namespace dexopt_chroot_setup { namespace { +using ::art::tools::CmdlineBuilder; + class DexoptChrootSetupTest : public CommonArtTest { protected: void SetUp() override { CommonArtTest::SetUp(); dexopt_chroot_setup_ = ndk::SharedRefBase::make<DexoptChrootSetup>(); + + // Note that if a real Pre-reboot Dexopt is kicked off after this check, the test will still + // fail, but that should be very rare. + if (std::filesystem::exists(DexoptChrootSetup::CHROOT_DIR)) { + GTEST_SKIP() << "A real Pre-reboot Dexopt is running"; + } + + test_skipped = false; + + scratch_dir_ = std::make_unique<ScratchDir>(); + scratch_path_ = scratch_dir_->GetPath(); + // Remove the trailing '/'; + scratch_path_.resize(scratch_path_.length() - 1); + } + + void TearDown() override { + if (test_skipped) { + return; + } + scratch_dir_.reset(); + dexopt_chroot_setup_->tearDown(); + CommonArtTest::TearDown(); } std::shared_ptr<DexoptChrootSetup> dexopt_chroot_setup_; + std::unique_ptr<ScratchDir> scratch_dir_; + std::string scratch_path_; + bool test_skipped = true; }; -TEST_F(DexoptChrootSetupTest, HelloWorld) { EXPECT_NE(dexopt_chroot_setup_, nullptr); } +TEST_F(DexoptChrootSetupTest, Run) { + // We only test the Mainline update case here. There isn't an easy way to test the OTA update case + // in such a unit test. The OTA update case is assumed to be covered by the E2E test. + ASSERT_TRUE(dexopt_chroot_setup_->setUp(/*in_otaSlot=*/std::nullopt).isOk()); + + // Some important dirs that should be the same as outside. + std::vector<const char*> same_dirs = { + "/", + "/system", + "/system_ext", + "/vendor", + "/product", + "/data", + "/mnt/expand", + "/dev", + "/dev/cpuctl", + "/dev/cpuset", + "/proc", + "/sys", + "/sys/fs/cgroup", + "/sys/fs/selinux", + "/metadata", + }; + + for (const std::string& dir : same_dirs) { + struct stat st_outside; + ASSERT_EQ(stat(dir.c_str(), &st_outside), 0); + struct stat st_inside; + ASSERT_EQ(stat(PathInChroot(dir).c_str(), &st_inside), 0); + EXPECT_EQ(st_outside.st_dev, st_inside.st_dev); + EXPECT_EQ(st_outside.st_ino, st_inside.st_ino); + } + + // Some important dirs that are expected to be writable. + std::vector<const char*> writable_dirs = { + "/data", + "/mnt/expand", + }; + + for (const std::string& dir : writable_dirs) { + EXPECT_EQ(access(PathInChroot(dir).c_str(), W_OK), 0); + } + + // Some important dirs that are not the same as outside but should be prepared. + std::vector<const char*> prepared_dirs = { + "/apex/com.android.art", + "/linkerconfig/com.android.art", + }; + + for (const std::string& dir : prepared_dirs) { + EXPECT_FALSE(std::filesystem::is_empty(PathInChroot(dir))); + } + + EXPECT_TRUE(std::filesystem::is_directory(PathInChroot("/mnt/artd_tmp"))); + + // Check that the chroot environment is capable to run programs. `dex2oat` is arbitrarily picked + // here. The test dex file and the scratch dir in /data are the same inside the chroot as outside. + CmdlineBuilder args; + args.Add(GetArtBinDir() + "/art_exec") + .Add("--chroot=%s", DexoptChrootSetup::CHROOT_DIR) + .Add("--") + .Add(GetArtBinDir() + "/dex2oat" + (Is64BitInstructionSet(kRuntimeISA) ? "64" : "32")) + .Add("--dex-file=%s", GetTestDexFileName("Main")) + .Add("--oat-file=%s", scratch_path_ + "/output.odex") + .Add("--output-vdex=%s", scratch_path_ + "/output.vdex") + .Add("--compiler-filter=speed") + .Add("--boot-image=/nonx/boot.art"); + std::string error_msg; + EXPECT_TRUE(Exec(args.Get(), &error_msg)) << error_msg; + + // Check that `setUp` can be repetitively called, to simulate the case where an instance of the + // caller (typically system_server) called `setUp` and crashed later, and a new instance called + // `setUp` again. + ASSERT_TRUE(dexopt_chroot_setup_->setUp(/*in_otaSlot=*/std::nullopt).isOk()); + + ASSERT_TRUE(dexopt_chroot_setup_->tearDown().isOk()); + + EXPECT_FALSE(std::filesystem::exists(DexoptChrootSetup::CHROOT_DIR)); + + // Check that `tearDown` can be repetitively called too. + ASSERT_TRUE(dexopt_chroot_setup_->tearDown().isOk()); +} } // namespace } // namespace dexopt_chroot_setup |