master ohmu/fastfetch-openbsd / src / common / android / binder.h
ohmu  ·  2026-10-15
  1#pragma once
  2
  3// Android exposes no public C API for the system services that carry the data fastfetch wants: the
  4// NDK has no BatteryManager / WifiManager, libbinder_ndk only speaks to AIDL HALs (it refuses to
  5// prepare a transaction for a binder that has no NDK class), and `/system/bin/service call` costs a
  6// fork/exec worth 15-20 ms. What is left is /dev/binder itself, which is all that `service` uses
  7// underneath. This header speaks that protocol directly: open() + mmap() + BINDER_WRITE_READ, no
  8// extra shared library, no child process, ~0.13 ms per call once a handle is cached.
  9//
 10// Three details are easy to get wrong. All three were confirmed against the device's own
 11// libbinder.so and with a ptrace trace of `service`, and getting any of them wrong makes the
 12// service answer BAD_TYPE (0x80000001) or BR_FAILED_REPLY (0x7211):
 13//
 14//   * A request parcel must start with three int32 values -- strict mode policy, work source and
 15//     'SYST'. Parcel::markForBinder() writes nothing for kernel binder, but the receiving side's
 16//     Parcel::enforceInterface() still compares the third word against its own header.
 17//   * Parcel::writeString16() writes the length, the UTF-16 code units, a UTF-16 NUL terminator and
 18//     then pads to four bytes. Skipping the terminator shifts every following field by four bytes.
 19//   * A handle arriving in a reply is owned by that reply buffer. It has to be acquired
 20//     (BC_ACQUIRE + BC_INCREFS) before BC_FREE_BUFFER; otherwise only a weak reference survives and
 21//     every later transaction on it fails, because the kernel looks up a strong reference.
 22
 23#include "fastfetch.h" // IWYU pragma: keep
 24
 25// The structures and command words below are not defined here: bionic generates this header from
 26// bionic/libc/kernel/uapi/linux/android/binder.h and every NDK ships it, so it is the
 27// authoritative copy of the binder ABI -- struct binder_write_read, struct binder_transaction_data,
 28// struct flat_binder_object, BINDER_WRITE_READ / BINDER_VERSION, the BC_* and BR_* command words,
 29// TF_* and BINDER_TYPE_*. Using it beats transcribing it: a second copy can only be wrong, and if
 30// the header itself is wrong that is the NDK's problem to fix, not something fastfetch could have
 31// caught by keeping its own.
 32#include <linux/android/binder.h>
 33
 34#include <string.h>
 35#include <sys/ioctl.h>
 36
 37#define FF_BINDER_DEVICE "/dev/binder"
 38
 39// The kernel allocates reply payloads out of the region handed to mmap(); 1 MiB is what libbinder
 40// asks for. Replies themselves are small -- `listServices` on this device is the largest at a few
 41// hundred bytes -- but the region also carries the per-process buffer pool.
 42#define FF_BINDER_SHARED_SIZE (1024 * 1024)
 43
 44// A command word plus the binder_transaction_data that follows it. The payload never lands here:
 45// it lives in the mmap region, and BR_REPLY only hands over a pointer to it.
 46#define FF_BINDER_REPLY_BUFFER_SIZE 1024
 47
 48// How many handles a reply may transfer to us. Replies that carry more than this still succeed, but
 49// the surplus handles are left unacquired and will fail on first use.
 50#define FF_BINDER_MAX_HANDLES 4
 51
 52// How many file descriptors a reply may hand over. A descriptor is not a handle: the kernel installs
 53// it in the caller's own fd table as it delivers the reply, so it is owned rather than borrowed and
 54// the caller closes it. A reply carrying more than this still succeeds, and the surplus is closed
 55// again by ffBinderTransact() because nothing would be left holding it.
 56#define FF_BINDER_MAX_FDS 4
 57
 58// The three int32 values Parcel::writeInterfaceToken() puts in front of the descriptor, and that
 59// Parcel::enforceInterface() checks the last of. These come from libbinder rather than from the
 60// kernel, which is why they are spelled out here.
 61#define FF_BINDER_STRICT_MODE_PENALTY_GATHER 0x80000000u // (1 << 31), Parcel.cpp
 62#define FF_BINDER_UNSET_WORK_SOURCE 0xffffffffu          // IPCThreadState::kUnsetWorkSource
 63#define FF_BINDER_HEADER 0x53595354u                     // B_PACK_CHARS('S', 'Y', 'S', 'T')
 64
 65typedef struct FFBinder {
 66    int fd;
 67    uint8_t* shared;
 68    size_t sharedSize;
 69    int32_t protocolVersion;
 70} FFBinder;
 71
 72// Opens /dev/binder, checks the protocol version and maps the shared region. Returns nullptr on
 73// success, a static message otherwise; the device is left closed on failure.
 74[[gnu::nonnull(1), nodiscard]] const char* ffBinderOpen(FFBinder* binder);
 75
 76// Usable as a cleanup attribute. Safe on a zeroed or already closed FFBinder.
 77[[gnu::nonnull(1)]] void ffBinderClose(FFBinder* binder);
 78
 79// ---------------------------------------------------------------------------------------------
 80// Parcel writing. The caller owns the buffer; anything that does not fit is dropped and recorded
 81// in `truncated`, which ffBinderTransact() then reports as an error.
 82// ---------------------------------------------------------------------------------------------
 83
 84typedef struct FFBinderParcel {
 85    uint8_t* data;
 86    size_t capacity;
 87    size_t size;
 88    bool truncated;
 89} FFBinderParcel;
 90
 91[[nodiscard]] static inline FFBinderParcel ffBinderParcelCreate(uint8_t* data, size_t capacity) {
 92    return (FFBinderParcel) { .data = data, .capacity = capacity, .size = 0, .truncated = false };
 93}
 94
 95[[gnu::nonnull(1), nodiscard]] static inline uint8_t* ffBinderParcelReserve(FFBinderParcel* parcel, size_t bytes) {
 96    if (bytes > parcel->capacity - parcel->size) {
 97        parcel->truncated = true;
 98        return nullptr;
 99    }
100    uint8_t* result = parcel->data + parcel->size;
101    parcel->size += bytes;
102    return result;
103}
104
105[[gnu::nonnull(1)]] static inline void ffBinderParcelPutU32(FFBinderParcel* parcel, uint32_t value) {
106    uint8_t* dst = ffBinderParcelReserve(parcel, sizeof(uint32_t));
107    if (dst != nullptr) {
108        memcpy(dst, &value, sizeof(uint32_t));
109    }
110}
111
112[[gnu::nonnull(1)]] static inline void ffBinderParcelPutI32(FFBinderParcel* parcel, int32_t value) {
113    uint8_t* dst = ffBinderParcelReserve(parcel, sizeof(int32_t));
114    if (dst != nullptr) {
115        memcpy(dst, &value, sizeof(int32_t));
116    }
117}
118
119[[gnu::nonnull(1)]] static inline void ffBinderParcelPutU64(FFBinderParcel* parcel, uint64_t value) {
120    uint8_t* dst = ffBinderParcelReserve(parcel, sizeof(uint64_t));
121    if (dst != nullptr) {
122        memcpy(dst, &value, sizeof(uint64_t));
123    }
124}
125
126// AIDL string16. The descriptors and service names fastfetch asks for are ASCII, so one byte is one
127// UTF-16 code unit; anything else would have to be encoded properly here.
128[[gnu::nonnull(1, 2)]] static inline void ffBinderParcelPutString16(FFBinderParcel* parcel, const char* value) {
129    const size_t length = strlen(value);
130    const size_t payload = sizeof(uint32_t) + (length + 1) * 2; // length, code units, NUL terminator
131    const size_t padded = (payload + 3) & ~(size_t) 3;
132    uint8_t* dst = ffBinderParcelReserve(parcel, padded);
133    if (dst == nullptr) {
134        return;
135    }
136
137    memset(dst, 0, padded);
138    const uint32_t length32 = (uint32_t) length;
139    memcpy(dst, &length32, sizeof(uint32_t));
140    for (size_t i = 0; i < length; i++) {
141        dst[sizeof(uint32_t) + i * 2] = (uint8_t) value[i];
142    }
143    // The two bytes after the last code unit stay zero: that is the UTF-16 NUL terminator, and
144    // Parcel::readInplace() advances by (length + 1) * sizeof(char16_t) to account for it.
145}
146
147[[gnu::nonnull(1, 2)]] static inline void ffBinderParcelPutInterfaceToken(FFBinderParcel* parcel, const char* descriptor) {
148    ffBinderParcelPutU32(parcel, FF_BINDER_STRICT_MODE_PENALTY_GATHER);
149    ffBinderParcelPutU32(parcel, FF_BINDER_UNSET_WORK_SOURCE);
150    ffBinderParcelPutU32(parcel, FF_BINDER_HEADER);
151    ffBinderParcelPutString16(parcel, descriptor);
152}
153
154// ---------------------------------------------------------------------------------------------
155// Transactions
156// ---------------------------------------------------------------------------------------------
157
158typedef struct FFBinderReply {
159    uint8_t* data;    // caller owned; receives the payload
160    size_t capacity;  // ditto
161    size_t size;      // payload length in bytes
162    uint32_t code;    // transaction code the service echoed back
163    uint32_t flags;   // TF_* of the reply
164    uint32_t handleCount;
165    uint32_t handles[FF_BINDER_MAX_HANDLES];
166    uint32_t fdCount;
167    int32_t fds[FF_BINDER_MAX_FDS]; // owned by the caller, which closes them
168} FFBinderReply;
169
170[[nodiscard]] static inline FFBinderReply ffBinderReplyCreate(uint8_t* data, size_t capacity) {
171    return (FFBinderReply) { .data = data, .capacity = capacity, .size = 0, .code = 0, .flags = 0, .handleCount = 0, .handles = {}, .fdCount = 0, .fds = {} };
172}
173
174// A reply carrying TF_STATUS_CODE is not an AIDL parcel at all: the first four bytes are the raw
175// status_t that onTransact() returned (-74 being UNKNOWN_TRANSACTION, i.e. no such method). Check
176// this before interpreting anything else.
177[[gnu::nonnull(1), nodiscard]] static inline bool ffBinderReplyIsStatus(const FFBinderReply* reply) {
178    return (reply->flags & TF_STATUS_CODE) != 0;
179}
180
181[[gnu::nonnull(1), nodiscard]] static inline uint32_t ffBinderReadU32(const uint8_t* data, size_t size, size_t offset) {
182    uint32_t value = 0;
183    if (offset + sizeof(uint32_t) <= size) {
184        memcpy(&value, data + offset, sizeof(uint32_t));
185    }
186    return value;
187}
188
189[[gnu::nonnull(1), nodiscard]] static inline int32_t ffBinderReadI32(const uint8_t* data, size_t size, size_t offset) {
190    int32_t value = 0;
191    if (offset + sizeof(int32_t) <= size) {
192        memcpy(&value, data + offset, sizeof(int32_t));
193    }
194    return value;
195}
196
197[[gnu::nonnull(1), nodiscard]] static inline uint64_t ffBinderReadU64(const uint8_t* data, size_t size, size_t offset) {
198    uint64_t value = 0;
199    if (offset + sizeof(uint64_t) <= size) {
200        memcpy(&value, data + offset, sizeof(uint64_t));
201    }
202    return value;
203}
204
205// Synchronous transaction. Returns nullptr on success, a static message otherwise.
206//
207// `flags` carries the TF_* values for the request, and one of them is not cosmetic: a reply that
208// carries a file descriptor is only delivered at all when the request asked with TF_ACCEPT_FDS. A
209// request that did not gets BR_FAILED_REPLY instead, which reads like a service that is not running
210// rather than like a missing flag. TF_ONE_WAY is the other one the kernel acts on, and it is what
211// makes a call fire-and-forget -- there is then no reply to fill `reply` from.
212//
213// Handles the reply carries are acquired and added to `reply->handles` before the reply buffer is
214// freed; descriptors are added to `reply->fds`, which the caller owns and closes.
215//
216// `handle` 0 is the service manager; every other handle comes from ffBinderLookupService(). A
217// service that never replies blocks this call forever -- none of the services fastfetch reads does,
218// but there is no timeout here to fall back on.
219[[gnu::nonnull(1, 5, 6), nodiscard]] const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, const FFBinderParcel* parcel, FFBinderReply* reply);
220
221// ---------------------------------------------------------------------------------------------
222// Service manager
223// ---------------------------------------------------------------------------------------------
224
225#define FF_BINDER_SERVICE_MANAGER_HANDLE 0u
226#define FF_BINDER_SM_DESCRIPTOR "android.os.IServiceManager"
227
228// Entry points of android.os.IServiceManager, as this device answers them. The numbers do not match
229// the AOSP sources: checkService is 4 here, not 3. FF_BINDER_SM_GET_SERVICE returns a nullable
230// IBinder and is the simplest reply to parse; FF_BINDER_SM_CHECK_SERVICE is what `service` itself
231// calls and answers with a parcelable instead.
232#define FF_BINDER_SM_GET_SERVICE 1u
233#define FF_BINDER_SM_GET_SERVICE2 2u
234#define FF_BINDER_SM_CHECK_SERVICE 4u
235#define FF_BINDER_SM_LIST_SERVICES 6u
236
237// Resolves a service name to a handle through the service manager. Returns nullptr on success.
238[[gnu::nonnull(1, 2, 4), nodiscard]] const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t transactionCode, uint32_t* handle);
239
240// A handle resolved through the service manager, held together with the binder it came from so that a
241// single cleanup attribute can give it back on every path out of the function that took it. Zeroed --
242// or left at handle 0 by a lookup that has not run or did not succeed -- it is a no-op, which is what
243// makes it safe to declare above the lookup itself.
244typedef struct FFBinderServiceHandle {
245    FFBinder* binder;
246    uint32_t handle;
247} FFBinderServiceHandle;
248
249// Returns the strong and weak references ffBinderLookupService() acquired on the handle. Usable as a
250// cleanup attribute; safe on a zeroed or already released FFBinderServiceHandle.
251[[gnu::nonnull(1)]] void ffBinderServiceHandleRelease(FFBinderServiceHandle* service);
252
253// ---------------------------------------------------------------------------------------------
254// Handles a transaction acquired
255// ---------------------------------------------------------------------------------------------
256
257// Whatever ffBinderTransact() collected for the caller, released together on the way out of a
258// function -- the handles it acquired, plus the descriptors it delivered, which are owned rather
259// than borrowed and would otherwise stay open for the life of the process.
260//
261// Unlike FFBinderServiceHandle this cannot be declared before the transaction runs, because which
262// handles came back is only known afterwards. `replied` is therefore a separate flag the caller
263// raises once ffBinderTransact() has returned: it acquires the handles even when it goes on to fail
264// (a reply that did not fit the caller buffer still had its BC_ACQUIRE tail sent), so releasing
265// without it would drop a reference that was never taken.
266typedef struct FFBinderAcquired {
267    FFBinder* binder;
268    const uint32_t* handles;
269    uint32_t handleCount;
270    const int32_t* fds;
271    uint32_t fdCount;
272    const bool* replied;
273} FFBinderAcquired;
274
275// Usable as a cleanup attribute; safe on a zeroed FFBinderAcquired, and on one whose transaction did
276// not reply.
277[[gnu::nonnull(1)]] void ffBinderAcquiredRelease(FFBinderAcquired* acquired);