/* * 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::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 propertyFile = SysPath() / property; std::string propertyValue; if (!ReadFileToString(propertyFile, &propertyValue)) { return ErrnoError() << "Fail to read"; } return Trim(propertyValue); } 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& mountInfo) { const auto& tokens = Split(mountInfo, " "); if (tokens.size() < 2) { return std::make_pair("", ""); } return std::make_pair(tokens[0], tokens[1]); } std::pair parseMountPoint(const std::string& mountPoint) { auto packageId = fs::path(mountPoint).filename(); auto split = Split(packageId, "@"); if (split.size() == 2) { int version; if (!ParseInt(split[1], &version)) { version = -1; } return std::make_pair(split[0], version); } return std::make_pair(packageId, -1); } bool isActiveMountPoint(const std::string& mountPoint) { return (mountPoint.find('@') == std::string::npos); } Result PopulateLoopInfo(const BlockDevice& top_device, 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 if (slaves.size() == 2) { if (!StartsWith(backing_files[0], kActiveApexPackagesDataDir)) { std::swap(slaves[0], slaves[1]); std::swap(backing_files[0], backing_files[1]); } } if (!StartsWith(backing_files[0], kActiveApexPackagesDataDir)) { return Error() << "Data loop device " << slaves[0].DevPath() << " has unexpected backing file " << backing_files[0]; } if (slaves.size() == 2) { if (!StartsWith(backing_files[1], kApexHashTreeDir)) { 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& mountPoint) { // Now, see if it is dm-verity or loop mounted switch (block.GetType()) { case LoopDevice: { auto backingFile = block.GetProperty("loop/backing_file"); if (!backingFile.ok()) { return backingFile.error(); } auto result = MountedApexData(block.DevPath(), *backingFile, mountPoint, /* device_name= */ "", /* hashtree_loop_name= */ ""); NormalizeIfDeleted(&result); return result; } case DeviceMapperDevice: { auto name = block.GetProperty("dm/name"); if (!name.ok()) { return name.error(); } MountedApexData result; result.mount_point = mountPoint; result.device_name = *name; if (auto status = PopulateLoopInfo(block, &result); !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() { LOG(INFO) << "Populating APEX database from mounts..."; std::unordered_map activeVersions; std::ifstream mounts("/proc/mounts"); std::string line; while (std::getline(mounts, line)) { auto [block, mountPoint] = parseMountInfo(line); // TODO(b/158469914): distinguish between temp and non-temp mounts if (fs::path(mountPoint).parent_path() != kApexRoot) { continue; } if (isActiveMountPoint(mountPoint)) { continue; } auto mountData = resolveMountInfo(BlockDevice(block), mountPoint); if (!mountData.ok()) { LOG(WARNING) << "Can't resolve mount info " << mountData.error(); continue; } auto [package, version] = parseMountPoint(mountPoint); AddMountedApex(package, false, *mountData); auto active = activeVersions[package] < version; if (active) { activeVersions[package] = version; SetLatest(package, mountData->full_path); } LOG(INFO) << "Found " << mountPoint << " backed by" << (mountData->deleted ? " deleted " : " ") << "file " << mountData->full_path; } LOG(INFO) << mounted_apexes_.size() << " packages restored."; } } // namespace apex } // namespace android