summaryrefslogtreecommitdiff
path: root/dexopt_chroot_setup
diff options
context:
space:
mode:
author Jiakai Zhang <jiakaiz@google.com> 2024-03-28 18:33:17 +0000
committer Jiakai Zhang <jiakaiz@google.com> 2024-04-11 14:51:33 +0000
commit69dc24557f951ce2513d0ea77f35a499fa58467b (patch)
treea3eb82a23fcab08399ba31696c60740620369c80 /dexopt_chroot_setup
parent53ca944020bb86199f6f80d8594d5deb1b1d46dd (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.bp50
-rw-r--r--dexopt_chroot_setup/README.md63
-rw-r--r--dexopt_chroot_setup/art_standalone_dexopt_chroot_setup_tests.xml53
-rw-r--r--dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl13
-rw-r--r--dexopt_chroot_setup/dexopt_chroot_setup.cc449
-rw-r--r--dexopt_chroot_setup/dexopt_chroot_setup.h18
-rw-r--r--dexopt_chroot_setup/dexopt_chroot_setup_test.cc118
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