From 5c604b9a737453a93572a8f459b56bf62122f9cd Mon Sep 17 00:00:00 2001 From: Bart Van Assche Date: Mon, 29 Jul 2024 14:22:31 -0700 Subject: [PATCH] libhealthloop: Only wake up for power supply events healthd receives power supply information as uevents and holds a wakelock while receiving these uevents. Without uevent filter, suspend is postponed indefinitely if a uevent is generated during suspend. Fix this by attaching a BPF program to the uevent socket that filters out all events that are not power supply events. This CL replaces the following CLs: * Lianwei Wang, healthd: Don't set all eventpoll wakeup-able, 2015-07-09 (https://android-review.googlesource.com/c/platform/system/core/+/158851). * Stephane Lee, Add BPF filter to filter uevents for SUBSYSTEM=powersupply, 2022-05-06. Multiple ideas and some code in this CL have been borrowed from Stephane Lee's CL. Bug: 139203596 Bug: 140330870 Bug: 203131934 Bug: 203229817 Bug: 203462310 Bug: 221725014 Test: Verified that a Pixel 2024 still wakes up if a USB cable is connected or disconnected. Test: Verified that suspend and resume still works for a Pixel 2024 device. Test: Verified that the following text appears in the Cuttlefish logcat output: "HealthLoop: Successfully attached BPF program to uevent socket" Test: Verified as follows that recovery mode works fine with Cuttlefish: adb reboot recovery && adb root && adb shell dmesg | grep -E 'F DEBUG|HealthLoop' Change-Id: I64446b103d660d220880461bdef7ef0f531e1734 Signed-off-by: Bart Van Assche --- health/utils/libhealthloop/Android.bp | 39 ++++ health/utils/libhealthloop/HealthLoop.cpp | 42 +++- .../libhealthloop/filterPowerSupplyEvents.c | 87 ++++++++ .../filterPowerSupplyEventsTest.cpp | 207 ++++++++++++++++++ .../libhealthloop/include/health/HealthLoop.h | 2 + 5 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 health/utils/libhealthloop/filterPowerSupplyEvents.c create mode 100644 health/utils/libhealthloop/filterPowerSupplyEventsTest.cpp diff --git a/health/utils/libhealthloop/Android.bp b/health/utils/libhealthloop/Android.bp index b266f67bd8..4ebc57512f 100644 --- a/health/utils/libhealthloop/Android.bp +++ b/health/utils/libhealthloop/Android.bp @@ -21,6 +21,17 @@ package { default_applicable_licenses: ["hardware_interfaces_license"], } +bpf { + name: "filterPowerSupplyEvents.o", + srcs: ["filterPowerSupplyEvents.c"], + // "vendor: true" because all binaries that use this BPF filter are vendor + // binaries. + vendor: true, +} + +// Since "required" sections are ignored in static library definitions, +// filterPowerSupplyEvents.o has been added in +// build/make/target/product/base_vendor.mk. cc_library_static { name: "libhealthloop", vendor_available: true, @@ -34,6 +45,7 @@ cc_library_static { "libcutils", ], header_libs: [ + "bpf_headers", "libbatteryservice_headers", "libhealthd_headers", "libutils_headers", @@ -42,3 +54,30 @@ cc_library_static { "include", ], } + +genrule { + name: "filterPowerSupplyEvents.h", + out: ["filterPowerSupplyEvents.h"], + srcs: [":filterPowerSupplyEvents.o"], + cmd: "cat $(in) | od -v -tx1 | cut -c9- | grep -v '^$$' | sed 's/^/0x/;s/ /, 0x/g;s/^, //;s/$$/,/' > $(out)", +} + +cc_test_host { + name: "filterPowerSupplyEventsTest", + team: "trendy_team_pixel_system_sw_storage", + srcs: [ + "filterPowerSupplyEventsTest.cpp", + ], + shared_libs: [ + "libbase", + "libbpf", + ], + static_libs: [ + "libgmock", + ], + generated_headers: [ + "filterPowerSupplyEvents.h", + "libbpf_headers", + ], + compile_multilib: "64", +} diff --git a/health/utils/libhealthloop/HealthLoop.cpp b/health/utils/libhealthloop/HealthLoop.cpp index f8c3490cbe..70b774572d 100644 --- a/health/utils/libhealthloop/HealthLoop.cpp +++ b/health/utils/libhealthloop/HealthLoop.cpp @@ -30,8 +30,12 @@ #include #include +#include #include +using android::base::ErrnoError; +using android::base::Result; +using android::base::unique_fd; using namespace android; using namespace std::chrono_literals; @@ -116,7 +120,6 @@ void HealthLoop::PeriodicChores() { ScheduleBatteryUpdate(); } -// TODO(b/140330870): Use BPF instead. #define UEVENT_MSG_LEN 2048 void HealthLoop::UeventEvent(uint32_t epevents) { // No need to lock because uevent_fd_ is guaranteed to be initialized. @@ -152,8 +155,26 @@ void HealthLoop::UeventEvent(uint32_t epevents) { } } +// Attach a BPF filter to the @uevent_fd file descriptor. This fails in recovery mode because BPF is +// not supported in recovery mode. This fails for kernel versions 5.4 and before because the BPF +// program is rejected by the BPF verifier of older kernels. +Result HealthLoop::AttachFilter(int uevent_fd) { + static const char prg[] = + "/sys/fs/bpf/vendor/prog_filterPowerSupplyEvents_skfilter_power_supply"; + int filter_fd(bpf::retrieveProgram(prg)); + if (filter_fd < 0) { + return ErrnoError() << "failed to load BPF program " << prg; + } + if (setsockopt(uevent_fd, SOL_SOCKET, SO_ATTACH_BPF, &filter_fd, sizeof(filter_fd)) < 0) { + close(filter_fd); + return ErrnoError() << "failed to attach BPF program"; + } + close(filter_fd); + return {}; +} + void HealthLoop::UeventInit(void) { - uevent_fd_.reset(uevent_open_socket(64 * 1024, true)); + uevent_fd_.reset(uevent_create_socket(64 * 1024, true)); if (uevent_fd_ < 0) { KLOG_ERROR(LOG_TAG, "uevent_init: uevent_open_socket failed\n"); @@ -161,8 +182,25 @@ void HealthLoop::UeventInit(void) { } fcntl(uevent_fd_, F_SETFL, O_NONBLOCK); + + Result attach_result = AttachFilter(uevent_fd_); + if (!attach_result.ok()) { + std::string error_msg = attach_result.error().message(); + error_msg += + ". This is expected in recovery mode and also for kernel versions before 5.10."; + KLOG_WARNING(LOG_TAG, "%s", error_msg.c_str()); + } else { + KLOG_INFO(LOG_TAG, "Successfully attached the BPF filter to the uevent socket"); + } + if (RegisterEvent(uevent_fd_, &HealthLoop::UeventEvent, EVENT_WAKEUP_FD)) KLOG_ERROR(LOG_TAG, "register for uevent events failed\n"); + + if (uevent_bind(uevent_fd_.get()) < 0) { + uevent_fd_.reset(); + KLOG_ERROR(LOG_TAG, "uevent_init: binding socket failed\n"); + return; + } } void HealthLoop::WakeAlarmEvent(uint32_t /*epevents*/) { diff --git a/health/utils/libhealthloop/filterPowerSupplyEvents.c b/health/utils/libhealthloop/filterPowerSupplyEvents.c new file mode 100644 index 0000000000..5296993177 --- /dev/null +++ b/health/utils/libhealthloop/filterPowerSupplyEvents.c @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 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 // load_word() +#include // struct __sk_buff +#include // struct nlmsghdr +#include // uint32_t + +// M4: match 4 bytes. Returns 0 if all bytes match. +static inline uint32_t M4(struct __sk_buff* skb, unsigned int offset, uint8_t c0, uint8_t c1, + uint8_t c2, uint8_t c3) { + return load_word(skb, offset) ^ ((c0 << 24) | (c1 << 16) | (c2 << 8) | c3); +} + +// M2: match 2 bytes. Returns 0 if all bytes match. +static inline uint16_t M2(struct __sk_buff* skb, unsigned int offset, uint8_t c0, uint8_t c1) { + return load_half(skb, offset) ^ ((c0 << 8) | c1); +} + +// M1: match 1 byte. Returns 0 in case of a match. +static inline uint8_t M1(struct __sk_buff* skb, unsigned int offset, uint8_t c0) { + return load_byte(skb, offset) ^ c0; +} + +// Match "\0SUBSYSTEM=". Returns 0 in case of a match. +#define MATCH_SUBSYSTEM_LENGTH 11 +static inline uint32_t match_subsystem(struct __sk_buff* skb, unsigned int offset) { + return M4(skb, offset + 0, '\0', 'S', 'U', 'B') | M4(skb, offset + 4, 'S', 'Y', 'S', 'T') | + M2(skb, offset + 8, 'E', 'M') | M1(skb, offset + 10, '='); +} + +// Match "power_supply\0". Returns 0 in case of a match. +#define MATCH_POWER_SUPPLY_LENGTH 13 +static inline uint32_t match_power_supply(struct __sk_buff* skb, unsigned int offset) { + return M4(skb, offset + 0, 'p', 'o', 'w', 'e') | M4(skb, offset + 4, 'r', '_', 's', 'u') | + M4(skb, offset + 8, 'p', 'p', 'l', 'y') | M1(skb, offset + 12, '\0'); +} + +// The Linux kernel 5.4 BPF verifier rejects this program, probably because of its size. Hence the +// restriction that the kernel version must be at least 5.10. +DEFINE_BPF_PROG_KVER("skfilter/power_supply", AID_ROOT, AID_SYSTEM, filterPowerSupplyEvents, + KVER(5, 10, 0)) +(struct __sk_buff* skb) { + uint32_t i; + + // The first character matched by match_subsystem() is a '\0'. Starting + // right past the netlink message header is fine since the SUBSYSTEM= text + // never occurs at the start. See also the kobject_uevent_env() implementation: + // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/lib/kobject_uevent.c?#n473 + // The upper bound of this loop has been chosen not to exceed the maximum + // number of instructions in a BPF program (BPF loops are unrolled). + for (i = sizeof(struct nlmsghdr); i < 256; ++i) { + if (i + MATCH_SUBSYSTEM_LENGTH > skb->len) { + break; + } + if (match_subsystem(skb, i) == 0) { + goto found_subsystem; + } + } + + // The SUBSYSTEM= text has not been found in the bytes that have been + // examined: let the user space software perform filtering. + return skb->len; + +found_subsystem: + i += MATCH_SUBSYSTEM_LENGTH; + if (i + MATCH_POWER_SUPPLY_LENGTH <= skb->len && match_power_supply(skb, i) == 0) { + return skb->len; + } + return 0; +} + +LICENSE("Apache 2.0"); +CRITICAL("healthd"); diff --git a/health/utils/libhealthloop/filterPowerSupplyEventsTest.cpp b/health/utils/libhealthloop/filterPowerSupplyEventsTest.cpp new file mode 100644 index 0000000000..e885f0b749 --- /dev/null +++ b/health/utils/libhealthloop/filterPowerSupplyEventsTest.cpp @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2024 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 +#include +#include +#include // SO_ATTACH_BPF +#include +#include +#include +#include +#include +#include + +#define ASSERT_UNIX_OK(e) ASSERT_GE(e, 0) << strerror(errno) + +// TODO(bvanassche): remove the code below. See also b/357099095. +#ifndef SO_ATTACH_BPF +#define SO_ATTACH_BPF 50 // From . +#endif + +using ::android::base::unique_fd; +using ::testing::ScopedTrace; + +struct test_data { + bool discarded; + std::string_view str; +}; + +static const uint8_t binary_bpf_prog[] = { +#include "filterPowerSupplyEvents.h" +}; + +static std::vector>* msg_vec; + +std::ostream& operator<<(std::ostream& os, const test_data& td) { + os << "{.discarded=" << td.discarded << ", .str="; + for (auto c : td.str) { + if (isprint(c)) { + os << c; + } else { + os << "."; + } + } + return os << '}'; +} + +#define RECORD_ERR_MSG(fmt, ...) \ + do { \ + char* str; \ + if (asprintf(&str, fmt, ##__VA_ARGS__) < 0) break; \ + auto st = std::make_unique(__FILE__, __LINE__, str); \ + msg_vec->emplace_back(std::move(st)); \ + free(str); \ + } while (0) + +int libbpf_print_fn(enum libbpf_print_level, const char* fmt, va_list args) { + char* str; + if (vasprintf(&str, fmt, args) < 0) { + return 0; + } + msg_vec->emplace_back(std::make_unique(__FILE__, -1, str)); + free(str); + return 0; +} + +static void record_libbpf_output() { + libbpf_set_print(libbpf_print_fn); +} + +class filterPseTest : public testing::TestWithParam {}; + +struct ConnectedSockets { + unique_fd write_fd; + unique_fd read_fd; +}; + +// socketpair() only supports AF_UNIX sockets. AF_UNIX sockets do not +// support BPF filters. Hence connect two TCP sockets with each other. +static ConnectedSockets ConnectSockets(int domain, int type, int protocol) { + int _server_fd = socket(domain, type, protocol); + if (_server_fd < 0) { + return {}; + } + unique_fd server_fd(_server_fd); + + int _write_fd = socket(domain, type, protocol); + if (_write_fd < 0) { + RECORD_ERR_MSG("socket: %s", strerror(errno)); + return {}; + } + unique_fd write_fd(_write_fd); + + struct sockaddr_in sa = {.sin_family = AF_INET, .sin_addr.s_addr = INADDR_ANY}; + if (bind(_server_fd, (const struct sockaddr*)&sa, sizeof(sa)) < 0) { + RECORD_ERR_MSG("bind: %s", strerror(errno)); + return {}; + } + if (listen(_server_fd, 1) < 0) { + RECORD_ERR_MSG("listen: %s", strerror(errno)); + return {}; + } + socklen_t addr_len = sizeof(sa); + if (getsockname(_server_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + RECORD_ERR_MSG("getsockname: %s", strerror(errno)); + return {}; + } + errno = 0; + if (connect(_write_fd, (const struct sockaddr*)&sa, sizeof(sa)) < 0 && errno != EINPROGRESS) { + RECORD_ERR_MSG("connect: %s", strerror(errno)); + return {}; + } + int _read_fd = accept(_server_fd, NULL, NULL); + if (_read_fd < 0) { + RECORD_ERR_MSG("accept: %s", strerror(errno)); + return {}; + } + unique_fd read_fd(_read_fd); + + return {.write_fd = std::move(write_fd), .read_fd = std::move(read_fd)}; +} + +TEST_P(filterPseTest, filterPse) { + if (getuid() != 0) { + GTEST_SKIP() << "Must be run as root."; + return; + } + if (!msg_vec) { + msg_vec = new typeof(*msg_vec); + } + std::unique_ptr clear_msg_vec_at_end_of_scope(new int, [](int* p) { + msg_vec->clear(); + delete p; + }); + record_libbpf_output(); + + auto connected_sockets = ConnectSockets(AF_INET, SOCK_STREAM, 0); + unique_fd write_fd = std::move(connected_sockets.write_fd); + unique_fd read_fd = std::move(connected_sockets.read_fd); + + ASSERT_UNIX_OK(fcntl(read_fd, F_SETFL, O_NONBLOCK)); + + bpf_object* obj = bpf_object__open_mem(binary_bpf_prog, sizeof(binary_bpf_prog), NULL); + ASSERT_TRUE(obj) << "bpf_object__open() failed" << strerror(errno); + + // Find the BPF program within the object. + bpf_program* prog = bpf_object__find_program_by_name(obj, "filterPowerSupplyEvents"); + ASSERT_TRUE(prog); + + ASSERT_UNIX_OK(bpf_program__set_type(prog, BPF_PROG_TYPE_SOCKET_FILTER)); + + ASSERT_UNIX_OK(bpf_object__load(obj)); + + int filter_fd = bpf_program__fd(prog); + ASSERT_UNIX_OK(filter_fd); + + int setsockopt_result = + setsockopt(read_fd, SOL_SOCKET, SO_ATTACH_BPF, &filter_fd, sizeof(filter_fd)); + ASSERT_UNIX_OK(setsockopt_result); + + const test_data param = GetParam(); + const std::string header(sizeof(struct nlmsghdr), '\0'); + ASSERT_EQ(header.length(), sizeof(struct nlmsghdr)); + const std::string data = header + std::string(param.str); + const size_t len = data.length(); + std::cerr.write(data.data(), data.length()); + std::cerr << ")\n"; + ASSERT_EQ(write(write_fd, data.data(), len), len); + std::array read_buf; + int bytes_read = read(read_fd, read_buf.data(), read_buf.size()); + if (bytes_read < 0) { + ASSERT_EQ(errno, EAGAIN); + bytes_read = 0; + } else { + ASSERT_LT(bytes_read, read_buf.size()); + } + EXPECT_EQ(bytes_read, param.discarded ? 0 : len); + + bpf_object__close(obj); +} + +INSTANTIATE_TEST_SUITE_P( + filterPse, filterPseTest, + testing::Values(test_data{false, "a"}, + test_data{true, std::string_view("abc\0SUBSYSTEM=block\0", 20)}, + test_data{true, std::string_view("\0SUBSYSTEM=block", 16)}, + test_data{true, std::string_view("\0SUBSYSTEM=power_supply", 23)}, + test_data{false, std::string_view("\0SUBSYSTEM=power_supply\0", 24)}, + test_data{ + false, + "012345678901234567890123456789012345678901234567890123456789012345" + "678901234567890123456789012345678901234567890123456789012345678901" + "234567890123456789012345678901234567890123456789012345678901234567" + "890123456789012345678901234567890123456789\0SUBSYSTEM=block\0"})); diff --git a/health/utils/libhealthloop/include/health/HealthLoop.h b/health/utils/libhealthloop/include/health/HealthLoop.h index fc3066e4c6..1af7274729 100644 --- a/health/utils/libhealthloop/include/health/HealthLoop.h +++ b/health/utils/libhealthloop/include/health/HealthLoop.h @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -87,6 +88,7 @@ class HealthLoop { }; int InitInternal(); + static android::base::Result AttachFilter(int uevent_fd); void MainLoop(); void WakeAlarmInit(); void WakeAlarmEvent(uint32_t);