| /* |
| * 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 "status_or.h" |
| #include "string_log.h" |
| |
| #include <android-base/file.h> |
| #include <android-base/logging.h> |
| #include <android-base/parseint.h> |
| #include <android-base/strings.h> |
| |
| #include <filesystem> |
| #include <fstream> |
| #include <string> |
| #include <unordered_map> |
| #include <utility> |
| |
| using android::base::EndsWith; |
| using android::base::ParseInt; |
| using android::base::ReadFileToString; |
| 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; |
| |
| // from art/runtime/class_linker.cc |
| inline size_t hash_combine(size_t seed, size_t val) { |
| return seed ^ (val + 0x9e3779b9 + (seed << 6) + (seed >> 2)); |
| } |
| |
| typedef std::pair<dev_t, ino_t> inode_t; |
| struct inode_hash { |
| size_t operator()(const inode_t& inode) const { |
| auto h1 = std::hash<dev_t>{}(inode.first); |
| auto h2 = std::hash<ino_t>{}(inode.second); |
| return hash_combine(h1, h2); |
| } |
| }; |
| typedef std::unordered_map<inode_t, std::string, inode_hash> inode_map; |
| |
| 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; } |
| |
| StatusOr<std::string> GetProperty(const std::string& property) const { |
| auto propertyFile = SysPath() / property; |
| std::string propertyValue; |
| if (!ReadFileToString(propertyFile, &propertyValue)) { |
| return StatusOr<std::string>::MakeError(PStringLog() << "Fail to read"); |
| } |
| return StatusOr<std::string>(Trim(propertyValue)); |
| } |
| |
| std::vector<BlockDevice> GetSlaves() const { |
| std::vector<BlockDevice> 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.ErrorMessage(); |
| } |
| return slaves; |
| } |
| }; |
| |
| std::pair<fs::path, fs::path> 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<std::string, int> 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); |
| } |
| |
| StatusOr<inode_t> inodeFor(const std::string& path) { |
| struct stat buf; |
| if (stat(path.c_str(), &buf)) { |
| return StatusOr<inode_t>::MakeError(PStringLog() << "stat failed"); |
| } |
| return StatusOr<inode_t>(buf.st_dev, buf.st_ino); |
| } |
| |
| // Flattened packages from builtin APEX dirs(/system/apex, /product/apex, ...) |
| inode_map scanFlattendedPackages() { |
| inode_map map; |
| |
| for (const auto& dir : kApexPackageBuiltinDirs) { |
| WalkDir(dir, [&](const fs::directory_entry& entry) { |
| const auto& path = entry.path(); |
| if (isFlattenedApex(path)) { |
| auto inode = inodeFor(path); |
| if (inode.Ok()) { |
| map[*inode] = path; |
| } |
| } |
| }); |
| } |
| |
| return map; |
| } |
| |
| StatusOr<MountedApexData> resolveMountInfo(const BlockDevice& block, |
| const std::string& mountPoint, |
| const inode_map& inodeMap) { |
| auto Error = [](auto e) { return StatusOr<MountedApexData>::MakeError(e); }; |
| |
| // First, see if it is bind-mount'ed to a flattened APEX |
| // This is checked first since flattened APEXes can be located in any stacked |
| // filesystem. (e.g. if / is mounted via /dev/loop1, then /proc/mounts shows |
| // that loop device as associated block device. But it is not related to APEX |
| // activation.) In any cases, comparing (dev,inode) pair with scanned |
| // flattened APEXes must identify bind-mounted APEX properly. |
| // See b/131924899. |
| auto inode = inodeFor(mountPoint); |
| if (inode.Ok()) { |
| auto iter = inodeMap.find(*inode); |
| if (iter != inodeMap.end()) { |
| return StatusOr<MountedApexData>("", iter->second, 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 Error(backingFile.ErrorStatus()); |
| } |
| return StatusOr<MountedApexData>(block.DevPath(), *backingFile, |
| mountPoint, ""); |
| } |
| case DeviceMapperDevice: { |
| auto name = block.GetProperty("dm/name"); |
| if (!name.Ok()) { |
| return Error(name.ErrorStatus()); |
| } |
| auto slaves = block.GetSlaves(); |
| if (slaves.empty() || slaves[0].GetType() != LoopDevice) { |
| return Error("DeviceMapper device with no loop devices"); |
| } |
| // TODO(jooyung): handle multiple loop devices when hash tree is |
| // externalized |
| auto slave = slaves[0]; |
| auto backingFile = slave.GetProperty("loop/backing_file"); |
| if (!backingFile.Ok()) { |
| return Error(backingFile.ErrorStatus()); |
| } |
| return StatusOr<MountedApexData>(slave.DevPath(), *backingFile, |
| mountPoint, *name); |
| } |
| case UnknownDevice: { |
| return Error("Can't resolve " + block.DevPath().string()); |
| } |
| } |
| } |
| |
| } // namespace |
| |
| // On startup, APEX database is populated from /proc/mounts. |
| |
| // /apex/<package-id> can be mounted from |
| // - /dev/block/loopX : loop device |
| // - /dev/block/dm-X : dm-verity |
| // - <flattened> : bind-mount |
| |
| // 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. |
| |
| // In case of <flattened>, it is --bind mounted to a flattened |
| // APEX directory. This is allowed only for system/product |
| // partitions. So, original APEX directory can be found |
| // by comparing dev/inode pair with candidates. |
| |
| // 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<std::string, int> activeVersions; |
| inode_map inodeToFlattendApexMap = scanFlattendedPackages(); |
| |
| std::ifstream mounts("/proc/mounts"); |
| std::string line; |
| while (std::getline(mounts, line)) { |
| auto [block, mountPoint] = parseMountInfo(line); |
| // TODO(jooyung): ignore tmp mount? |
| if (fs::path(mountPoint).parent_path() != kApexRoot) { |
| continue; |
| } |
| if (isActiveMountPoint(mountPoint)) { |
| continue; |
| } |
| |
| auto mountData = resolveMountInfo(BlockDevice(block), mountPoint, |
| inodeToFlattendApexMap); |
| if (!mountData.Ok()) { |
| LOG(WARNING) << "Can't resolve mount info " << mountData.ErrorMessage(); |
| 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; |
| } |
| |
| LOG(INFO) << mounted_apexes_.size() << " packages restored."; |
| } |
| |
| } // namespace apex |
| } // namespace android |