/* * Copyright (C) 2019 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. */ #define LOG_TAG "apexd" #include "apex_database.h" #include "apex_constants.h" #include "apex_file.h" #include "apexd_utils.h" #include "string_log.h" #include #include #include #include #include #include #include #include #include #include using android::base::ConsumeSuffix; using android::base::EndsWith; using android::base::ErrnoError; using android::base::Error; using android::base::ParseInt; using android::base::ReadFileToString; using android::base::Result; using android::base::Split; using android::base::StartsWith; using android::base::Trim; namespace fs = std::filesystem; namespace android { namespace apex { namespace { using MountedApexData = MountedApexDatabase::MountedApexData; enum BlockDeviceType { UnknownDevice, LoopDevice, DeviceMapperDevice, }; const fs::path kDevBlock = "/dev/block"; const fs::path kSysBlock = "/sys/block"; class BlockDevice { std::string name; // loopN, dm-N, ... public: explicit BlockDevice(const fs::path& path) { name = path.filename(); } BlockDeviceType GetType() const { if (StartsWith(name, "loop")) return LoopDevice; if (StartsWith(name, "dm-")) return DeviceMapperDevice; return UnknownDevice; } fs::path SysPath() const { return kSysBlock / name; } fs::path DevPath() const { return kDevBlock / name; } Result GetProperty(const std::string& property) const { auto property_file = SysPath() / property; std::string property_value; if (!ReadFileToString(property_file, &property_value)) { return ErrnoError() << "Fail to read"; } return Trim(property_value); } std::vector GetSlaves() const { std::vector slaves; std::error_code ec; auto status = WalkDir(SysPath() / "slaves", [&](const auto& entry) { BlockDevice dev(entry); if (fs::is_block_file(dev.DevPath(), ec)) { slaves.push_back(dev); } }); if (!status.ok()) { LOG(WARNING) << status.error(); } return slaves; } }; std::pair ParseMountInfo(const std::string& mount_info) { const auto& tokens = Split(mount_info, " "); if (tokens.size() < 2) { return std::make_pair("", ""); } return std::make_pair(tokens[0], tokens[1]); } std::pair ParseMountPoint(const std::string& mount_point) { auto package_id = fs::path(mount_point).filename(); auto split = Split(package_id, "@"); if (split.size() == 2) { int version; if (!ParseInt(split[1], &version)) { version = -1; } return std::make_pair(split[0], version); } return std::make_pair(package_id, -1); } bool IsActiveMountPoint(const std::string& mount_point) { return (mount_point.find('@') == std::string::npos); } Result PopulateLoopInfo(const BlockDevice& top_device, const std::string& active_apex_dir, const std::string& decompression_dir, const std::string& apex_hash_tree_dir, MountedApexData* apex_data) { std::vector slaves = top_device.GetSlaves(); if (slaves.size() != 1 && slaves.size() != 2) { return Error() << "dm device " << top_device.DevPath() << " has unexpected number of slaves : " << slaves.size(); } std::vector backing_files; backing_files.reserve(slaves.size()); for (const auto& dev : slaves) { if (dev.GetType() != LoopDevice) { return Error() << dev.DevPath() << " is not a loop device"; } auto backing_file = dev.GetProperty("loop/backing_file"); if (!backing_file.ok()) { return backing_file.error(); } backing_files.push_back(std::move(*backing_file)); } // Enforce following invariant: // * slaves[0] always represents a data loop device // * if size = 2 then slaves[1] represents an external hashtree loop device auto is_data_loop_device = [&](const std::string& backing_file) { return StartsWith(backing_file, active_apex_dir) || StartsWith(backing_file, decompression_dir); }; if (slaves.size() == 2) { if (!is_data_loop_device(backing_files[0])) { std::swap(slaves[0], slaves[1]); std::swap(backing_files[0], backing_files[1]); } } if (!is_data_loop_device(backing_files[0])) { return Error() << "Data loop device " << slaves[0].DevPath() << " has unexpected backing file " << backing_files[0]; } if (slaves.size() == 2) { if (!StartsWith(backing_files[1], apex_hash_tree_dir)) { return Error() << "Hashtree loop device " << slaves[1].DevPath() << " has unexpected backing file " << backing_files[1]; } apex_data->hashtree_loop_name = slaves[1].DevPath(); } apex_data->loop_name = slaves[0].DevPath(); apex_data->full_path = backing_files[0]; return {}; } // This is not the right place to do this normalization, but proper solution // will require some refactoring first. :( // TODO(b/158469911): introduce MountedApexDataBuilder and delegate all // building/normalization logic to it. void NormalizeIfDeleted(MountedApexData* apex_data) { std::string_view full_path = apex_data->full_path; if (ConsumeSuffix(&full_path, "(deleted)")) { apex_data->deleted = true; auto it = full_path.rbegin(); while (it != full_path.rend() && isspace(*it)) { it++; } full_path.remove_suffix(it - full_path.rbegin()); } else { apex_data->deleted = false; } apex_data->full_path = full_path; } Result ResolveMountInfo( const BlockDevice& block, const std::string& mount_point, const std::string& active_apex_dir, const std::string& decompression_dir, const std::string& apex_hash_tree_dir) { bool temp_mount = EndsWith(mount_point, ".tmp"); // Now, see if it is dm-verity or loop mounted switch (block.GetType()) { case LoopDevice: { auto backing_file = block.GetProperty("loop/backing_file"); if (!backing_file.ok()) { return backing_file.error(); } auto result = MountedApexData(block.DevPath(), *backing_file, mount_point, /* device_name= */ "", /* hashtree_loop_name= */ "", /* is_temp_mount */ temp_mount); NormalizeIfDeleted(&result); return result; } case DeviceMapperDevice: { auto name = block.GetProperty("dm/name"); if (!name.ok()) { return name.error(); } MountedApexData result; result.mount_point = mount_point; result.device_name = *name; result.is_temp_mount = temp_mount; auto status = PopulateLoopInfo(block, active_apex_dir, decompression_dir, apex_hash_tree_dir, &result); if (!status.ok()) { return status.error(); } NormalizeIfDeleted(&result); return result; } case UnknownDevice: { return Errorf("Can't resolve {}", block.DevPath().string()); } } } } // namespace // On startup, APEX database is populated from /proc/mounts. // /apex/ can be mounted from // - /dev/block/loopX : loop device // - /dev/block/dm-X : dm-verity // In case of loop device, it is from a non-flattened // APEX file. This original APEX file can be tracked // by /sys/block/loopX/loop/backing_file. // In case of dm-verity, it is mapped to a loop device. // This mapped loop device can be traced by // /sys/block/dm-X/slaves/ directory which contains // a symlink to /sys/block/loopY, which leads to // the original APEX file. // Device name can be retrieved from // /sys/block/dm-Y/dm/name. // By synchronizing the mounts info with Database on startup, // Apexd serves the correct package list even on the devices // which are not ro.apex.updatable. void MountedApexDatabase::PopulateFromMounts( const std::string& active_apex_dir, const std::string& decompression_dir, const std::string& apex_hash_tree_dir) REQUIRES(!mounted_apexes_mutex_) { LOG(INFO) << "Populating APEX database from mounts..."; std::unordered_map active_versions; std::ifstream mounts("/proc/mounts"); std::string line; std::lock_guard lock(mounted_apexes_mutex_); while (std::getline(mounts, line)) { auto [block, mount_point] = ParseMountInfo(line); // TODO(b/158469914): distinguish between temp and non-temp mounts if (fs::path(mount_point).parent_path() != kApexRoot) { continue; } if (IsActiveMountPoint(mount_point)) { continue; } auto mount_data = ResolveMountInfo(BlockDevice(block), mount_point, active_apex_dir, decompression_dir, apex_hash_tree_dir); if (!mount_data.ok()) { LOG(WARNING) << "Can't resolve mount info " << mount_data.error(); continue; } auto [package, version] = ParseMountPoint(mount_point); AddMountedApexLocked(package, false, *mount_data); auto active = active_versions[package] < version; if (active) { active_versions[package] = version; SetLatestLocked(package, mount_data->full_path); } LOG(INFO) << "Found " << mount_point << " backed by" << (mount_data->deleted ? " deleted " : " ") << "file " << mount_data->full_path; } LOG(INFO) << mounted_apexes_.size() << " packages restored."; } } // namespace apex } // namespace android