You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
10 KiB
312 lines
10 KiB
/*
|
|
* 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 <android-base/file.h>
|
|
#include <android-base/logging.h>
|
|
#include <android-base/parseint.h>
|
|
#include <android-base/result.h>
|
|
#include <android-base/strings.h>
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
|
|
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<std::string> 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<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.error();
|
|
}
|
|
return slaves;
|
|
}
|
|
};
|
|
|
|
std::pair<fs::path, fs::path> 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<std::string, int> 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<void> 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<BlockDevice> 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<std::string> 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<MountedApexData> 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/<package-id> 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<std::string, int> 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
|