The current mechanism for specifying boot devices on Android systems
involves passing a set of "boot_devices" though command line,
bootconfig, or device tree.
The bootdevices are specified as strings and, in general, need to
match a sysfs path but without the "/sys/devices" or
"/sys/devices/platform" prefix. The sysfs path is generally the path
to the closest parent of the block device that is a "platform" device.
As an example, if the sysfs path of the expected boot device is:
/sys/devices/platform/soc@0/7c4000.mmc/mmc_host/mmc1/mmc1:0001/block/mmcblk1
The bootloader would specify it as "soc@0/7c4000.mmc" since:
* We strip off "/sys/devices/platform/"
* As we move up directories, we don't find one whose subsystem is
"platform" until we get up to
"/sys/devices/platform/soc@0/7c4000.mmc".
The current mechanism is a bit brittle. Specifically:
* The sysfs path isn't _really_ stable and can change across kernel
upgrades. For instance, during one kernel upgrade the device tree
for a product changed so that the root node changed from "soc" to
"soc@0" and this changed all sysfs paths. In the past device tree
folks have asserted that we shouldn't rely on dts node names to stay
consistent, yet those node names are used to construct sysfs paths.
* For some devices, like USB, the path of the closest "platform"
device tends to be the path of the USB controller. This means that
if two USB disks are plugged in we can't guarantee which one will be
identified as the boot device.
Add a new method of finding the boot device by passing the partition
UUID that we loaded the kernel from. Using the partition UUID to
identify the boot device is standard on Linux. You can see this
because when you're not using an initramfs you can use the syntax
"root=PARTUUID=<valid-uuid-id>[/PARTNROFF=n]" to specify the root.
Using the same idea for Android's boot code makes sense.
With this new method for finding the boot device, we can make the code
much more specific about matching sysfs paths. Once we find the sysfs
path for the kernel we can make sure that all of the other boot
partition share the same "scsi" or "mmc" parent instead of going all
the way to the closest platform device. In the above example, this
means that we'd make sure that all boot devices are found under this
sysfs node:
/sys/devices/platform/soc@0/7c4000.mmc/mmc_host/mmc1/mmc1:0001/block/mmcblk1
...instead of just making sure they are under:
/sys/devices/platform/soc@0/7c4000.mmc
There is the question of what we should do if the bootloader passes
_both_ an old style "boot_devices" and also a partition UUID. In this
case, we'll issue a warning and then ignore the old "boot_devices".
Considering it a warning rather than an error could allow switching to
the "boot_part_uuid" method even if an old bootloader is still
hardcoding some old "boot_devices".
NOTE: Using partition UUID won't cause any security problems even
though someone _could_ plug in an external device crafted to have the
same UUID as the normal boot device's kernel partition. We already
have "verity" in the system making sure our filesystems are not
tampered with and this would also protect us from booting a tampered
disk. That means that the worst someone could do in this case would be
to confuse the system and make the device non-bootable. Chromebooks
have been using the partition UUID to find the root filesystems for
years and this has never been a problem.
NOTE: this new method relies on the commit ("init: Add partition_uuid
to Uevent") which in turn relies upstream kernel commit 74f4a8dc0dd8
("block: add partition uuid into uevent as "PARTUUID"").
Bug: 316324155
Test: Use partition UUID to boot
Change-Id: If824cb700ca3696a442a28e6ad02d7c522c3b495
414 lines
16 KiB
C++
414 lines
16 KiB
C++
/*
|
|
* Copyright (C) 2010 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.
|
|
*/
|
|
|
|
#include "ueventd.h"
|
|
|
|
#include <android/api-level.h>
|
|
#include <ctype.h>
|
|
#include <dirent.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <set>
|
|
#include <thread>
|
|
|
|
#include <android-base/chrono_utils.h>
|
|
#include <android-base/logging.h>
|
|
#include <android-base/properties.h>
|
|
#include <fstab/fstab.h>
|
|
#include <selinux/android.h>
|
|
#include <selinux/selinux.h>
|
|
|
|
#include "devices.h"
|
|
#include "firmware_handler.h"
|
|
#include "modalias_handler.h"
|
|
#include "selabel.h"
|
|
#include "selinux.h"
|
|
#include "uevent_handler.h"
|
|
#include "uevent_listener.h"
|
|
#include "ueventd_parser.h"
|
|
#include "util.h"
|
|
|
|
// At a high level, ueventd listens for uevent messages generated by the kernel through a netlink
|
|
// socket. When ueventd receives such a message it handles it by taking appropriate actions,
|
|
// which can typically be creating a device node in /dev, setting file permissions, setting selinux
|
|
// labels, etc.
|
|
// Ueventd also handles loading of firmware that the kernel requests, and creates symlinks for block
|
|
// and character devices.
|
|
|
|
// When ueventd starts, it regenerates uevents for all currently registered devices by traversing
|
|
// /sys and writing 'add' to each 'uevent' file that it finds. This causes the kernel to generate
|
|
// and resend uevent messages for all of the currently registered devices. This is done, because
|
|
// ueventd would not have been running when these devices were registered and therefore was unable
|
|
// to receive their uevent messages and handle them appropriately. This process is known as
|
|
// 'cold boot'.
|
|
|
|
// 'init' currently waits synchronously on the cold boot process of ueventd before it continues
|
|
// its boot process. For this reason, cold boot should be as quick as possible. One way to achieve
|
|
// a speed up here is to parallelize the handling of ueventd messages, which consume the bulk of the
|
|
// time during cold boot.
|
|
|
|
// Handling of uevent messages has two unique properties:
|
|
// 1) It can be done in isolation; it doesn't need to read or write any status once it is started.
|
|
// 2) It uses setegid() and setfscreatecon() so either care (aka locking) must be taken to ensure
|
|
// that no file system operations are done while the uevent process has an abnormal egid or
|
|
// fscreatecon or this handling must happen in a separate process.
|
|
// Given the above two properties, it is best to fork() subprocesses to handle the uevents. This
|
|
// reduces the overhead and complexity that would be required in a solution with threads and locks.
|
|
// In testing, a racy multithreaded solution has the same performance as the fork() solution, so
|
|
// there is no reason to deal with the complexity of the former.
|
|
|
|
// One other important caveat during the boot process is the handling of SELinux restorecon.
|
|
// Since many devices have child devices, calling selinux_android_restorecon() recursively for each
|
|
// device when its uevent is handled, results in multiple restorecon operations being done on a
|
|
// given file. It is more efficient to simply do restorecon recursively on /sys during cold boot,
|
|
// than to do restorecon on each device as its uevent is handled. This only applies to cold boot;
|
|
// once that has completed, restorecon is done for each device as its uevent is handled.
|
|
|
|
// With all of the above considered, the cold boot process has the below steps:
|
|
// 1) ueventd regenerates uevents by doing the /sys traversal and listens to the netlink socket for
|
|
// the generated uevents. It writes these uevents into a queue represented by a vector.
|
|
//
|
|
// 2) ueventd forks 'n' separate uevent handler subprocesses and has each of them to handle the
|
|
// uevents in the queue based on a starting offset (their process number) and a stride (the total
|
|
// number of processes). Note that no IPC happens at this point and only const functions from
|
|
// DeviceHandler should be called from this context.
|
|
//
|
|
// 3) In parallel to the subprocesses handling the uevents, the main thread of ueventd calls
|
|
// selinux_android_restorecon() recursively on /sys/class, /sys/block, and /sys/devices.
|
|
//
|
|
// 4) Once the restorecon operation finishes, the main thread calls waitpid() to wait for all
|
|
// subprocess handlers to complete and exit. Once this happens, it marks coldboot as having
|
|
// completed.
|
|
//
|
|
// At this point, ueventd is single threaded, poll()'s and then handles any future uevents.
|
|
|
|
// Lastly, it should be noted that uevents that occur during the coldboot process are handled
|
|
// without issue after the coldboot process completes. This is because the uevent listener is
|
|
// paused while the uevent handler and restorecon actions take place. Once coldboot completes,
|
|
// the uevent listener resumes in polling mode and will handle the uevents that occurred during
|
|
// coldboot.
|
|
|
|
namespace android {
|
|
namespace init {
|
|
|
|
class ColdBoot {
|
|
public:
|
|
ColdBoot(UeventListener& uevent_listener,
|
|
std::vector<std::unique_ptr<UeventHandler>>& uevent_handlers,
|
|
bool enable_parallel_restorecon,
|
|
std::vector<std::string> parallel_restorecon_queue)
|
|
: uevent_listener_(uevent_listener),
|
|
uevent_handlers_(uevent_handlers),
|
|
num_handler_subprocesses_(std::thread::hardware_concurrency() ?: 4),
|
|
enable_parallel_restorecon_(enable_parallel_restorecon),
|
|
parallel_restorecon_queue_(parallel_restorecon_queue) {}
|
|
|
|
void Run();
|
|
|
|
private:
|
|
void UeventHandlerMain(unsigned int process_num, unsigned int total_processes);
|
|
void RegenerateUevents();
|
|
void ForkSubProcesses();
|
|
void WaitForSubProcesses();
|
|
void RestoreConHandler(unsigned int process_num, unsigned int total_processes);
|
|
void GenerateRestoreCon(const std::string& directory);
|
|
|
|
UeventListener& uevent_listener_;
|
|
std::vector<std::unique_ptr<UeventHandler>>& uevent_handlers_;
|
|
|
|
unsigned int num_handler_subprocesses_;
|
|
bool enable_parallel_restorecon_;
|
|
|
|
std::vector<Uevent> uevent_queue_;
|
|
|
|
std::set<pid_t> subprocess_pids_;
|
|
|
|
std::vector<std::string> restorecon_queue_;
|
|
|
|
std::vector<std::string> parallel_restorecon_queue_;
|
|
};
|
|
|
|
void ColdBoot::UeventHandlerMain(unsigned int process_num, unsigned int total_processes) {
|
|
for (unsigned int i = process_num; i < uevent_queue_.size(); i += total_processes) {
|
|
auto& uevent = uevent_queue_[i];
|
|
|
|
for (auto& uevent_handler : uevent_handlers_) {
|
|
uevent_handler->HandleUevent(uevent);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ColdBoot::RestoreConHandler(unsigned int process_num, unsigned int total_processes) {
|
|
android::base::Timer t_process;
|
|
|
|
for (unsigned int i = process_num; i < restorecon_queue_.size(); i += total_processes) {
|
|
android::base::Timer t;
|
|
auto& dir = restorecon_queue_[i];
|
|
|
|
selinux_android_restorecon(dir.c_str(), SELINUX_ANDROID_RESTORECON_RECURSE);
|
|
|
|
//Mark a dir restorecon operation for 50ms,
|
|
//Maybe you can add this dir to the ueventd.rc script to parallel processing
|
|
if (t.duration() > 50ms) {
|
|
LOG(INFO) << "took " << t.duration().count() <<"ms restorecon '"
|
|
<< dir.c_str() << "' on process '" << process_num <<"'";
|
|
}
|
|
}
|
|
|
|
//Calculate process restorecon time
|
|
LOG(VERBOSE) << "took " << t_process.duration().count() << "ms on process '"
|
|
<< process_num << "'";
|
|
}
|
|
|
|
void ColdBoot::GenerateRestoreCon(const std::string& directory) {
|
|
std::unique_ptr<DIR, decltype(&closedir)> dir(opendir(directory.c_str()), &closedir);
|
|
|
|
if (!dir) {
|
|
PLOG(WARNING) << "opendir " << directory.c_str();
|
|
return;
|
|
}
|
|
|
|
struct dirent* dent;
|
|
while ((dent = readdir(dir.get())) != NULL) {
|
|
if (strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0) continue;
|
|
|
|
struct stat st;
|
|
if (fstatat(dirfd(dir.get()), dent->d_name, &st, 0) == -1) continue;
|
|
|
|
if (S_ISDIR(st.st_mode)) {
|
|
std::string fullpath = directory + "/" + dent->d_name;
|
|
auto parallel_restorecon =
|
|
std::find(parallel_restorecon_queue_.begin(),
|
|
parallel_restorecon_queue_.end(), fullpath);
|
|
if (parallel_restorecon == parallel_restorecon_queue_.end()) {
|
|
restorecon_queue_.emplace_back(fullpath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ColdBoot::RegenerateUevents() {
|
|
uevent_listener_.RegenerateUevents([this](const Uevent& uevent) {
|
|
uevent_queue_.emplace_back(uevent);
|
|
return ListenerAction::kContinue;
|
|
});
|
|
}
|
|
|
|
void ColdBoot::ForkSubProcesses() {
|
|
for (unsigned int i = 0; i < num_handler_subprocesses_; ++i) {
|
|
auto pid = fork();
|
|
if (pid < 0) {
|
|
PLOG(FATAL) << "fork() failed!";
|
|
}
|
|
|
|
if (pid == 0) {
|
|
UeventHandlerMain(i, num_handler_subprocesses_);
|
|
if (enable_parallel_restorecon_) {
|
|
RestoreConHandler(i, num_handler_subprocesses_);
|
|
}
|
|
_exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
subprocess_pids_.emplace(pid);
|
|
}
|
|
}
|
|
|
|
void ColdBoot::WaitForSubProcesses() {
|
|
// Treat subprocesses that crash or get stuck the same as if ueventd itself has crashed or gets
|
|
// stuck.
|
|
//
|
|
// When a subprocess crashes, we fatally abort from ueventd. init will restart ueventd when
|
|
// init reaps it, and the cold boot process will start again. If this continues to fail, then
|
|
// since ueventd is marked as a critical service, init will reboot to bootloader.
|
|
//
|
|
// When a subprocess gets stuck, keep ueventd spinning waiting for it. init has a timeout for
|
|
// cold boot and will reboot to the bootloader if ueventd does not complete in time.
|
|
while (!subprocess_pids_.empty()) {
|
|
int status;
|
|
pid_t pid = TEMP_FAILURE_RETRY(waitpid(-1, &status, 0));
|
|
if (pid == -1) {
|
|
PLOG(ERROR) << "waitpid() failed";
|
|
continue;
|
|
}
|
|
|
|
auto it = std::find(subprocess_pids_.begin(), subprocess_pids_.end(), pid);
|
|
if (it == subprocess_pids_.end()) continue;
|
|
|
|
if (WIFEXITED(status)) {
|
|
if (WEXITSTATUS(status) == EXIT_SUCCESS) {
|
|
subprocess_pids_.erase(it);
|
|
} else {
|
|
LOG(FATAL) << "subprocess exited with status " << WEXITSTATUS(status);
|
|
}
|
|
} else if (WIFSIGNALED(status)) {
|
|
LOG(FATAL) << "subprocess killed by signal " << WTERMSIG(status);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ColdBoot::Run() {
|
|
android::base::Timer cold_boot_timer;
|
|
|
|
RegenerateUevents();
|
|
|
|
if (enable_parallel_restorecon_) {
|
|
if (parallel_restorecon_queue_.empty()) {
|
|
parallel_restorecon_queue_.emplace_back("/sys");
|
|
// takes long time for /sys/devices, parallelize it
|
|
parallel_restorecon_queue_.emplace_back("/sys/devices");
|
|
LOG(INFO) << "Parallel processing directory is not set, set the default";
|
|
}
|
|
for (const auto& dir : parallel_restorecon_queue_) {
|
|
selinux_android_restorecon(dir.c_str(), 0);
|
|
GenerateRestoreCon(dir);
|
|
}
|
|
}
|
|
|
|
ForkSubProcesses();
|
|
|
|
if (!enable_parallel_restorecon_) {
|
|
selinux_android_restorecon("/sys", SELINUX_ANDROID_RESTORECON_RECURSE);
|
|
}
|
|
|
|
WaitForSubProcesses();
|
|
|
|
android::base::SetProperty(kColdBootDoneProp, "true");
|
|
LOG(INFO) << "Coldboot took " << cold_boot_timer.duration().count() / 1000.0f << " seconds";
|
|
}
|
|
|
|
static UeventdConfiguration GetConfiguration() {
|
|
if (IsMicrodroid()) {
|
|
return ParseConfig({"/system/etc/ueventd.rc", "/vendor/etc/ueventd.rc"});
|
|
}
|
|
|
|
auto hardware = android::base::GetProperty("ro.hardware", "");
|
|
|
|
struct LegacyPathInfo {
|
|
std::string legacy_path;
|
|
std::string preferred;
|
|
};
|
|
std::vector<LegacyPathInfo> legacy_paths{
|
|
{"/vendor/ueventd.rc", "/vendor/etc/ueventd.rc"},
|
|
{"/odm/ueventd.rc", "/odm/etc/ueventd.rc"},
|
|
{"/ueventd." + hardware + ".rc", "another ueventd.rc file"}};
|
|
|
|
std::vector<std::string> canonical{"/system/etc/ueventd.rc"};
|
|
|
|
if (android::base::GetIntProperty("ro.product.first_api_level", 10000) < __ANDROID_API_T__) {
|
|
// TODO: Remove these legacy paths once Android S is no longer supported.
|
|
for (const auto& info : legacy_paths) {
|
|
canonical.push_back(info.legacy_path);
|
|
}
|
|
} else {
|
|
// Warn if newer device is using legacy paths.
|
|
for (const auto& info : legacy_paths) {
|
|
if (access(info.legacy_path.c_str(), F_OK) == 0) {
|
|
LOG(FATAL_WITHOUT_ABORT)
|
|
<< "Legacy ueventd configuration file detected and will not be parsed: "
|
|
<< info.legacy_path << ". Please move your configuration to "
|
|
<< info.preferred << " instead.";
|
|
}
|
|
}
|
|
}
|
|
|
|
return ParseConfig(canonical);
|
|
}
|
|
|
|
int ueventd_main(int argc, char** argv) {
|
|
/*
|
|
* init sets the umask to 077 for forked processes. We need to
|
|
* create files with exact permissions, without modification by
|
|
* the umask.
|
|
*/
|
|
umask(000);
|
|
|
|
android::base::InitLogging(argv, &android::base::KernelLogger);
|
|
|
|
LOG(INFO) << "ueventd started!";
|
|
|
|
SelinuxSetupKernelLogging();
|
|
SelabelInitialize();
|
|
|
|
std::vector<std::unique_ptr<UeventHandler>> uevent_handlers;
|
|
|
|
auto ueventd_configuration = GetConfiguration();
|
|
|
|
UeventListener uevent_listener(ueventd_configuration.uevent_socket_rcvbuf_size);
|
|
|
|
// Right after making DeviceHandler, replay all events looking for which
|
|
// block device has the boot partition. This lets us make symlinks
|
|
// for all of the other partitions on the same disk. Note that by the time
|
|
// we get here we know that the boot partition has already shown up (if
|
|
// we're looking for it) so just regenerating events is enough to know
|
|
// we'll see it.
|
|
std::unique_ptr<DeviceHandler> device_handler = std::make_unique<DeviceHandler>(
|
|
std::move(ueventd_configuration.dev_permissions),
|
|
std::move(ueventd_configuration.sysfs_permissions),
|
|
std::move(ueventd_configuration.subsystems), android::fs_mgr::GetBootDevices(),
|
|
android::fs_mgr::GetBootPartUuid(), true);
|
|
uevent_listener.RegenerateUevents([&](const Uevent& uevent) -> ListenerAction {
|
|
bool uuid_check_done = device_handler->CheckUeventForBootPartUuid(uevent);
|
|
return uuid_check_done ? ListenerAction::kStop : ListenerAction::kContinue;
|
|
});
|
|
|
|
uevent_handlers.emplace_back(std::move(device_handler));
|
|
uevent_handlers.emplace_back(std::make_unique<FirmwareHandler>(
|
|
std::move(ueventd_configuration.firmware_directories),
|
|
std::move(ueventd_configuration.external_firmware_handlers)));
|
|
|
|
if (ueventd_configuration.enable_modalias_handling) {
|
|
std::vector<std::string> base_paths = {"/odm/lib/modules", "/vendor/lib/modules"};
|
|
uevent_handlers.emplace_back(std::make_unique<ModaliasHandler>(base_paths));
|
|
}
|
|
if (!android::base::GetBoolProperty(kColdBootDoneProp, false)) {
|
|
ColdBoot cold_boot(uevent_listener, uevent_handlers,
|
|
ueventd_configuration.enable_parallel_restorecon,
|
|
ueventd_configuration.parallel_restorecon_dirs);
|
|
cold_boot.Run();
|
|
}
|
|
|
|
for (auto& uevent_handler : uevent_handlers) {
|
|
uevent_handler->ColdbootDone();
|
|
}
|
|
|
|
// We use waitpid() in ColdBoot, so we can't ignore SIGCHLD until now.
|
|
signal(SIGCHLD, SIG_IGN);
|
|
// Reap and pending children that exited between the last call to waitpid() and setting SIG_IGN
|
|
// for SIGCHLD above.
|
|
while (waitpid(-1, nullptr, WNOHANG) > 0) {
|
|
}
|
|
|
|
// Restore prio before main loop
|
|
setpriority(PRIO_PROCESS, 0, 0);
|
|
uevent_listener.Poll([&uevent_handlers](const Uevent& uevent) {
|
|
for (auto& uevent_handler : uevent_handlers) {
|
|
uevent_handler->HandleUevent(uevent);
|
|
}
|
|
return ListenerAction::kContinue;
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
} // namespace init
|
|
} // namespace android
|