mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-29 00:03:58 +01:00
Wifi (Android): rewrites impl via /dev/binder
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
#include "common/android/dex.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
#include "common/library.h"
|
||||
#include <zlib.h>
|
||||
#endif
|
||||
|
||||
// Offsets into the dex header, from https://source.android.com/docs/core/runtime/dex-format.
|
||||
#define FF_DEX_ENDIAN_TAG 0x12345678u
|
||||
#define FF_DEX_HEADER_SIZE 0x70
|
||||
#define FF_DEX_OFF_FILE_SIZE 0x20
|
||||
#define FF_DEX_OFF_ENDIAN_TAG 0x28
|
||||
#define FF_DEX_OFF_STRING_IDS 0x3C
|
||||
#define FF_DEX_OFF_TYPE_IDS_SIZE 0x40
|
||||
#define FF_DEX_OFF_TYPE_IDS 0x44
|
||||
#define FF_DEX_OFF_FIELD_IDS 0x54
|
||||
#define FF_DEX_OFF_CLASS_DEFS_SIZE 0x60
|
||||
#define FF_DEX_OFF_CLASS_DEFS 0x64
|
||||
|
||||
// class_def_item: [u32 class_idx][u32 access_flags][u32 superclass_idx][u32 interfaces_off]
|
||||
// [u32 source_file_idx][u32 annotations_off][u32 class_data_off][u32 static_values_off]
|
||||
#define FF_DEX_CLASS_DEF_SIZE 32
|
||||
#define FF_DEX_OFF_CLASS_DEF_DATA 24
|
||||
#define FF_DEX_OFF_CLASS_DEF_STATIC_VALUES 28
|
||||
|
||||
// field_id_item: [u16 class_idx][u16 type_idx][u32 name_idx]
|
||||
#define FF_DEX_FIELD_ID_SIZE 8
|
||||
#define FF_DEX_OFF_FIELD_ID_NAME 4
|
||||
|
||||
// The zip local file header, from APPNOTE.TXT 4.3.7. The central directory would carry the same
|
||||
// fields, but the local header sits right in front of the data, so one walk over the file finds
|
||||
// both the bounds and the payload.
|
||||
#define FF_ZIP_LOCAL_HEADER_SIZE 30
|
||||
#define FF_ZIP_OFF_METHOD 8
|
||||
#define FF_ZIP_OFF_COMPRESSED_SIZE 18
|
||||
#define FF_ZIP_OFF_UNCOMPRESSED_SIZE 22
|
||||
#define FF_ZIP_OFF_NAME_LENGTH 26
|
||||
#define FF_ZIP_OFF_EXTRA_LENGTH 28
|
||||
#define FF_ZIP_METHOD_STORED 0
|
||||
#define FF_ZIP_METHOD_DEFLATED 8
|
||||
|
||||
#define FF_DEX_ENTRY "classes.dex"
|
||||
#define FF_DEX_MAGIC "dex\n"
|
||||
|
||||
static uint16_t dexU16(const uint8_t* p) {
|
||||
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
|
||||
}
|
||||
|
||||
static uint32_t dexU32(const uint8_t* p) {
|
||||
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
|
||||
}
|
||||
|
||||
// LEB128, at most five bytes for the 32-bit values a dex stores this way.
|
||||
static uint32_t dexUleb128(const uint8_t** p) {
|
||||
uint32_t value = 0;
|
||||
for (int shift = 0; shift <= 28; shift += 7) {
|
||||
const uint8_t byte = *(*p)++;
|
||||
value |= (uint32_t) (byte & 0x7f) << shift;
|
||||
if ((byte & 0x80) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// encoded_value: one header byte holding ((size - 1) << 5) | type, then that many payload bytes. A
|
||||
// `static final int` constant is written as VALUE_INT, whose payload is a sign-extended
|
||||
// little-endian integer. Reading through 64 bits keeps every shift in range even if the size field
|
||||
// is nonsense; the caller's length check is what rejects such a dex.
|
||||
static int32_t dexEncodedInt(const uint8_t** p) {
|
||||
const uint8_t header = *(*p)++;
|
||||
const uint32_t size = (uint32_t) (header >> 5) + 1;
|
||||
uint64_t value = 0;
|
||||
for (uint32_t i = 0; i < size && i < 8; ++i) {
|
||||
value |= (uint64_t) (*(*p)++) << (i * 8);
|
||||
}
|
||||
const uint32_t bits = size * 8;
|
||||
if (bits < 64 && (value & (1ull << (bits - 1))) != 0) {
|
||||
value |= ~((1ull << bits) - 1);
|
||||
}
|
||||
return (int32_t) (uint32_t) value;
|
||||
}
|
||||
|
||||
// string_data_item: a uleb128 length in UTF-16 code units, then MUTF-8 bytes and a NUL terminator.
|
||||
// The descriptors and field names looked up here are ASCII, where MUTF-8 and UTF-8 agree, so the
|
||||
// bytes can be used as they are.
|
||||
static const char* dexString(const uint8_t* dex, const uint8_t* end, uint32_t index) {
|
||||
const uint8_t* ids = dex + dexU32(dex + FF_DEX_OFF_STRING_IDS);
|
||||
if (ids < dex || (size_t) (end - ids) < (size_t) index * 4 + 4) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint8_t* p = dex + dexU32(ids + (size_t) index * 4);
|
||||
if (p < dex || p >= end) {
|
||||
return nullptr;
|
||||
}
|
||||
(void) dexUleb128(&p);
|
||||
if (p >= end) {
|
||||
return nullptr;
|
||||
}
|
||||
return memchr(p, '\0', (size_t) (end - p)) != nullptr ? (const char*) p : nullptr;
|
||||
}
|
||||
|
||||
// Walks the class's static field list and the class's static value array in step, and returns the
|
||||
// value paired with `fieldName`. Both lists are ordered by field index and cover exactly the same
|
||||
// fields -- when they do not, the pairing is not trustworthy and nothing is returned.
|
||||
static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* classDescriptor, const char* fieldName, int32_t* result) {
|
||||
const uint8_t* end = dex + size;
|
||||
|
||||
if (size < FF_DEX_HEADER_SIZE || dexU32(dex + FF_DEX_OFF_ENDIAN_TAG) != FF_DEX_ENDIAN_TAG) {
|
||||
return "Not a dex file";
|
||||
}
|
||||
const uint32_t fileSize = dexU32(dex + FF_DEX_OFF_FILE_SIZE);
|
||||
if (fileSize > size || fileSize < FF_DEX_HEADER_SIZE) {
|
||||
return "The dex file size is out of range";
|
||||
}
|
||||
end = dex + fileSize;
|
||||
|
||||
// The class's type index, so that the class_def_item and the field ids can be filtered by class.
|
||||
const uint8_t* types = dex + dexU32(dex + FF_DEX_OFF_TYPE_IDS);
|
||||
const uint32_t typeCount = dexU32(dex + FF_DEX_OFF_TYPE_IDS_SIZE);
|
||||
if (types < dex || (size_t) (end - types) < (size_t) typeCount * 4) {
|
||||
return "The dex type table is out of range";
|
||||
}
|
||||
uint32_t typeIndex = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < typeCount; ++i) {
|
||||
const char* descriptor = dexString(dex, end, dexU32(types + (size_t) i * 4));
|
||||
if (descriptor != nullptr && strcmp(descriptor, classDescriptor) == 0) {
|
||||
typeIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (typeIndex == UINT32_MAX) {
|
||||
return "The class is not in the dex";
|
||||
}
|
||||
|
||||
const uint8_t* classes = dex + dexU32(dex + FF_DEX_OFF_CLASS_DEFS);
|
||||
const uint32_t classCount = dexU32(dex + FF_DEX_OFF_CLASS_DEFS_SIZE);
|
||||
if (classes < dex || (size_t) (end - classes) < (size_t) classCount * FF_DEX_CLASS_DEF_SIZE) {
|
||||
return "The dex class table is out of range";
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < classCount; ++i) {
|
||||
const uint8_t* classDef = classes + (size_t) i * FF_DEX_CLASS_DEF_SIZE;
|
||||
if (dexU32(classDef) != typeIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t* fields = dex + dexU32(classDef + FF_DEX_OFF_CLASS_DEF_DATA);
|
||||
const uint8_t* values = dex + dexU32(classDef + FF_DEX_OFF_CLASS_DEF_STATIC_VALUES);
|
||||
if (fields < dex || fields >= end || values < dex || values >= end) {
|
||||
return "The dex class data is out of range";
|
||||
}
|
||||
|
||||
const uint32_t staticFields = dexUleb128(&fields);
|
||||
(void) dexUleb128(&fields); // instance_fields_size
|
||||
(void) dexUleb128(&fields); // direct_methods_size
|
||||
(void) dexUleb128(&fields); // virtual_methods_size
|
||||
const uint32_t staticValues = dexUleb128(&values);
|
||||
if (staticFields != staticValues) {
|
||||
return "The dex static fields and values do not pair up";
|
||||
}
|
||||
|
||||
const uint8_t* fieldIds = dex + dexU32(dex + FF_DEX_OFF_FIELD_IDS);
|
||||
uint32_t fieldIndex = 0;
|
||||
for (uint32_t j = 0; j < staticFields; ++j) {
|
||||
if (fields >= end) {
|
||||
return "The dex static field list is truncated";
|
||||
}
|
||||
fieldIndex += dexUleb128(&fields); // field_idx_diff
|
||||
(void) dexUleb128(&fields); // access_flags
|
||||
const int32_t value = dexEncodedInt(&values);
|
||||
|
||||
if ((size_t) (end - fieldIds) < ((size_t) fieldIndex + 1) * FF_DEX_FIELD_ID_SIZE) {
|
||||
return "The dex field table is out of range";
|
||||
}
|
||||
const char* name = dexString(dex, end, dexU32(fieldIds + (size_t) fieldIndex * FF_DEX_FIELD_ID_SIZE + FF_DEX_OFF_FIELD_ID_NAME));
|
||||
if (name != nullptr && strcmp(name, fieldName) == 0) {
|
||||
*result = value;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return "The field is not a static field of the class";
|
||||
}
|
||||
return "The class has no class_def_item";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Locating classes.dex
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
typedef struct FFDexMapping {
|
||||
uint8_t* mapped; // the jar, as mapped; nullptr once released
|
||||
size_t mappedSize;
|
||||
const uint8_t* data; // the dex bytes: inside `mapped`, or `inflated`
|
||||
size_t size;
|
||||
uint8_t* inflated; // owned; only set when the entry had to be decompressed
|
||||
} FFDexMapping;
|
||||
|
||||
static void wrapDexMapping(FFDexMapping* mapping) {
|
||||
assert(mapping);
|
||||
free(mapping->inflated);
|
||||
if (mapping->mapped != nullptr) {
|
||||
munmap(mapping->mapped, mapping->mappedSize);
|
||||
}
|
||||
}
|
||||
|
||||
// The local file header of `classes.dex` carries both the payload bounds and its compression, so a
|
||||
// single walk over the jar is enough to find it either way. AOSP builds framework jars with the
|
||||
// entry STORED so that ART can map it, which is the common case and needs no decompression.
|
||||
static const char* findDexEntry(const uint8_t* jar, size_t jarSize, const uint8_t** data, size_t* dataSize, uint32_t* uncompressedSize, uint16_t* method) {
|
||||
for (size_t i = 0; i + FF_ZIP_LOCAL_HEADER_SIZE <= jarSize; ++i) {
|
||||
if (memcmp(jar + i, "PK\x03\x04", 4) != 0) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t nameLength = dexU16(jar + i + FF_ZIP_OFF_NAME_LENGTH);
|
||||
const uint16_t extraLength = dexU16(jar + i + FF_ZIP_OFF_EXTRA_LENGTH);
|
||||
if (nameLength != sizeof(FF_DEX_ENTRY) - 1) {
|
||||
continue;
|
||||
}
|
||||
const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) nameLength + (size_t) extraLength;
|
||||
if (headerSize > jarSize - i) {
|
||||
continue;
|
||||
}
|
||||
if (memcmp(jar + i + FF_ZIP_LOCAL_HEADER_SIZE, FF_DEX_ENTRY, sizeof(FF_DEX_ENTRY) - 1) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t compressedSize = dexU32(jar + i + FF_ZIP_OFF_COMPRESSED_SIZE);
|
||||
if (compressedSize == 0 || compressedSize > jarSize - i - headerSize) {
|
||||
// A zero size means a data descriptor follows the payload instead of the header; the
|
||||
// magic scan in the caller still covers that case as long as the entry is STORED.
|
||||
continue;
|
||||
}
|
||||
*method = dexU16(jar + i + FF_ZIP_OFF_METHOD);
|
||||
*uncompressedSize = dexU32(jar + i + FF_ZIP_OFF_UNCOMPRESSED_SIZE);
|
||||
*data = jar + i + headerSize;
|
||||
*dataSize = compressedSize;
|
||||
return nullptr;
|
||||
}
|
||||
return "The jar has no usable classes.dex entry";
|
||||
}
|
||||
|
||||
// Fallback for a jar whose entry is STORED but whose header has no sizes: the dex magic is then a
|
||||
// literal run of bytes in the file, and the header that follows it validates or it does not.
|
||||
static const uint8_t* findDexMagic(const uint8_t* jar, size_t jarSize, size_t* dexSize) {
|
||||
for (size_t i = 0; i + FF_DEX_HEADER_SIZE <= jarSize; ++i) {
|
||||
if (memcmp(jar + i, FF_DEX_MAGIC, sizeof(FF_DEX_MAGIC) - 1) != 0) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t* dex = jar + i;
|
||||
const uint32_t fileSize = dexU32(dex + FF_DEX_OFF_FILE_SIZE);
|
||||
if (dexU32(dex + FF_DEX_OFF_ENDIAN_TAG) != FF_DEX_ENDIAN_TAG) {
|
||||
continue;
|
||||
}
|
||||
if (fileSize < FF_DEX_HEADER_SIZE || fileSize > jarSize - i) {
|
||||
continue;
|
||||
}
|
||||
*dexSize = fileSize;
|
||||
return dex;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
static const char* inflateDex(const uint8_t* data, size_t dataSize, uint32_t uncompressedSize, uint8_t** out) {
|
||||
FF_LIBRARY_LOAD(zlib, "dlopen(libz) failed", "libz" FF_LIBRARY_EXTENSION, 2)
|
||||
FF_LIBRARY_LOAD_SYMBOL(zlib, inflateInit2_, "dlsym(inflateInit2_) failed")
|
||||
FF_LIBRARY_LOAD_SYMBOL(zlib, inflate, "dlsym(inflate) failed")
|
||||
FF_LIBRARY_LOAD_SYMBOL(zlib, inflateEnd, "dlsym(inflateEnd) failed")
|
||||
|
||||
uint8_t* buffer = malloc(uncompressedSize);
|
||||
if (buffer == nullptr) {
|
||||
return "malloc failed";
|
||||
}
|
||||
|
||||
// `uncompress` is not usable here: a zip entry holds a raw deflate stream, without the two byte
|
||||
// zlib header that entry point insists on. A negative window size tells inflate to skip the
|
||||
// header, which is what the zip format expects.
|
||||
z_stream stream = {};
|
||||
stream.next_in = (Bytef*) data;
|
||||
stream.avail_in = (uInt) dataSize;
|
||||
stream.next_out = buffer;
|
||||
stream.avail_out = (uInt) uncompressedSize;
|
||||
|
||||
if (ffinflateInit2_(&stream, -MAX_WBITS, ZLIB_VERSION, (int) sizeof(z_stream)) != Z_OK) {
|
||||
free(buffer);
|
||||
return "inflateInit2 failed";
|
||||
}
|
||||
const int status = ffinflate(&stream, Z_FINISH);
|
||||
ffinflateEnd(&stream);
|
||||
if (status != Z_STREAM_END || stream.total_out != (uLong) uncompressedSize) {
|
||||
free(buffer);
|
||||
return "Inflating classes.dex failed";
|
||||
}
|
||||
|
||||
*out = buffer;
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, const char* fieldName, int32_t* result) {
|
||||
[[gnu::cleanup(wrapDexMapping)]] FFDexMapping mapping = {};
|
||||
|
||||
const int fd = open(jarPath, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
return "open(jar) failed";
|
||||
}
|
||||
|
||||
struct stat st = {};
|
||||
if (fstat(fd, &st) != 0 || st.st_size <= 0) {
|
||||
close(fd);
|
||||
return "fstat(jar) failed";
|
||||
}
|
||||
mapping.mappedSize = (size_t) st.st_size;
|
||||
mapping.mapped = mmap(nullptr, mapping.mappedSize, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
// The mapping outlives the descriptor, so the descriptor can go either way from here.
|
||||
close(fd);
|
||||
if (mapping.mapped == MAP_FAILED) {
|
||||
mapping.mapped = nullptr;
|
||||
return "mmap(jar) failed";
|
||||
}
|
||||
|
||||
const uint8_t* data = nullptr;
|
||||
size_t dataSize = 0;
|
||||
uint32_t uncompressedSize = 0;
|
||||
uint16_t method = FF_ZIP_METHOD_STORED;
|
||||
|
||||
if (findDexEntry(mapping.mapped, mapping.mappedSize, &data, &dataSize, &uncompressedSize, &method) != nullptr) {
|
||||
data = findDexMagic(mapping.mapped, mapping.mappedSize, &dataSize);
|
||||
if (data == nullptr) {
|
||||
return "No dex in the jar";
|
||||
}
|
||||
} else if (method == FF_ZIP_METHOD_STORED) {
|
||||
dataSize = uncompressedSize;
|
||||
} else if (method == FF_ZIP_METHOD_DEFLATED) {
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
const char* error = inflateDex(data, dataSize, uncompressedSize, &mapping.inflated);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
data = mapping.inflated;
|
||||
dataSize = uncompressedSize;
|
||||
#else
|
||||
return "The jar deflates classes.dex and fastfetch was built without zlib";
|
||||
#endif
|
||||
} else {
|
||||
return "classes.dex uses an unsupported compression method";
|
||||
}
|
||||
|
||||
mapping.data = data;
|
||||
mapping.size = dataSize;
|
||||
return dexStaticInt(mapping.data, mapping.size, classDescriptor, fieldName, result);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
// AIDL gives every method of an interface a transaction code at build time:
|
||||
//
|
||||
// static final int TRANSACTION_<method> = IBinder.FIRST_CALL_TRANSACTION + <declaration index>;
|
||||
//
|
||||
// so the number is a property of the `.aidl` of the release that built the device's jar, not of any
|
||||
// public header. Counting the methods in an AOSP checkout only describes that checkout, and it goes
|
||||
// wrong the moment a release or a vendor fork inserts a method ahead of the one being looked for:
|
||||
// `IWifiManager.getConnectionInfo` is 29 on Android 11 and 41 on Android 16. There is no negotiation
|
||||
// and no discovery -- a wrong code reaches a different method, or none.
|
||||
//
|
||||
// Being a compile-time constant, the value lands in the `static_values` array of the `X$Stub` class
|
||||
// in the jar's dex, paired position by position with that class's static field list. Reading the two
|
||||
// together gives the mapping of the build that is actually on the device, on any release, with
|
||||
// nothing to keep in sync.
|
||||
//
|
||||
// `libdexfile.so` cannot do this for us. It is dlopen-able from an app UID through the ART apex, but
|
||||
// it is not in the NDK and its only C ABI -- the `ADexFile_*` family -- covers methods, not fields;
|
||||
// the field and class-data APIs are C++ with no ABI guarantee.
|
||||
//
|
||||
// `ffReadFileBuffer` would work but pulls the whole jar through the heap for one integer. The jar is
|
||||
// mapped instead, and only the pages actually read are faulted in.
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// Resolves the int value of `<classDescriptor>.<fieldName>` out of the `classes.dex` of `jarPath`.
|
||||
// `classDescriptor` is the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;".
|
||||
// Returns nullptr on success, a static message otherwise.
|
||||
[[gnu::nonnull(1, 2, 3, 4), nodiscard]] const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, const char* fieldName, int32_t* result);
|
||||
@@ -1,33 +1,663 @@
|
||||
#include "wifi.h"
|
||||
|
||||
#include "common/processing.h"
|
||||
#include "common/properties.h"
|
||||
#include "common/android/api.h"
|
||||
#include "common/android/binder.h"
|
||||
#include "common/android/dex.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#define FF_TERMUX_API_PATH FASTFETCH_TARGET_DIR_ROOT "/libexec/termux-api"
|
||||
#define FF_TERMUX_API_PARAM "WifiConnectionInfo"
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static inline void wrapYyjsonFree(yyjson_doc** doc) {
|
||||
assert(doc);
|
||||
if (*doc) {
|
||||
yyjson_doc_free(*doc);
|
||||
// Android gives an app two ways to see the current connection, and both are weaker than the desktop
|
||||
// ones. `/system/bin/cmd wifi status` prints everything fastfetch wants, but it does not run in the
|
||||
// calling process: `cmd` hands the arguments to the service over binder and the command body runs
|
||||
// inside system_server, where it reads a locally built WifiInfo. An app can therefore not ask for
|
||||
// it -- the call comes back as a silent exit status 255 with nothing on either stream, which is
|
||||
// what makes it look like it worked. `dumpsys wifi` needs android.permission.DUMP, and
|
||||
// /proc/net/wireless, /proc/net/dev and /sys/class/net/wlan0 are all EACCES. That leaves
|
||||
// `IWifiManager.getConnectionInfo`, which needs only ACCESS_WIFI_STATE -- a normal permission,
|
||||
// granted without asking. There is no Termux:API fallback: it returned less than this does, and
|
||||
// needed a separate app installed.
|
||||
//
|
||||
// The transaction code is read out of the device's own jar rather than carried as a table, so the
|
||||
// module is not tied to a list of releases. It does need the jar to exist, and Android 11 is the
|
||||
// release that moved the Wi-Fi framework into an APEX -- before that the class sat in framework.jar,
|
||||
// which is not read here. So Android 11 (API 30) is the floor, and an older release reports an error
|
||||
// rather than a guess.
|
||||
//
|
||||
// The reply layout itself has been checked against the AOSP source as far back as Android 9, which
|
||||
// is why nothing about it is release-gated: the head grew a link speed in Android 10 and a Wi-Fi
|
||||
// standard in Android 11, and both are found by shape.
|
||||
//
|
||||
// Whether the radio is on is a second call, `IWifiManager.getWifiEnabledState`, which takes no
|
||||
// arguments and answers with a `WifiManager.WIFI_STATE_*`. It is needed because the interface
|
||||
// cannot answer for itself: bionic's `getifaddrs` lists only the interfaces that hold an address,
|
||||
// so the flags that would say whether `wlan0` is up leave the list exactly when there is no
|
||||
// association -- and nothing else in the reply separates a Wi-Fi that is off from one that is on
|
||||
// with nothing behind it. Both answer with an empty `WifiInfo`, the same at every offset, down to
|
||||
// the `02:00:00:00:00:00` placeholder it writes in place of a BSSID. The call is made only when the
|
||||
// interface was not in the list, so a connected device -- where `IFF_UP` is both closer to hand and
|
||||
// more accurate -- pays nothing for it.
|
||||
//
|
||||
// Three things about the call are positional rather than negotiated, and all three were measured
|
||||
// on a vivo V2505A (Android 16) and a Redmi 9A (Android 11):
|
||||
//
|
||||
// * The transaction code is a build-time constant of the `.aidl` (`getConnectionInfo` is 29 on
|
||||
// Android 11 and 41 on Android 16) -- see common/android/dex.h.
|
||||
// * The reply layout drifts, and not only by release. Android 12 dropped the duplicate length word
|
||||
// that Android 11 and 10 wrote in front of the SSID octets, and the vivo's Android 16 build
|
||||
// fills an int between the transmit and the receive speed that the Redmi's Android 11 build
|
||||
// leaves out -- while AOSP's own Android 16 writes the head exactly the Android 11 way, so that
|
||||
// one int is a vendor addition rather than a release difference. Frequency is the only channel
|
||||
// centre in the head, so it is located by value, with the whole result validated before it is
|
||||
// used. A wrong guess shows up as a rejected layout, not as a plausible wrong number.
|
||||
// * The tail of the parcel, which holds the Wi-Fi standard, is not laid out the same way by every
|
||||
// vendor: the Android 11 build on the Redmi carries three words there that AOSP's own Android 11
|
||||
// does not. It is located by shape for that reason.
|
||||
//
|
||||
// BSSID and MAC are the one place the binder route beats `cmd wifi status`: MAC addresses are
|
||||
// redacted for a caller targeting a recent SDK, and the shell targets the current one, so `cmd wifi
|
||||
// status` prints 24:**:**:**:70:2c where this reads 24:a4:87:3c:70:2c.
|
||||
//
|
||||
// What is deliberately left empty:
|
||||
//
|
||||
// * `security`. `WifiInfo` carries no security type on Android 11 at all, and on Android 16 the
|
||||
// value the shell command prints is not in the parcel either -- `cmd wifi status` shows it only
|
||||
// because it reads the object in system_server. No other call an app can make returns it, so
|
||||
// the field stays empty rather than being guessed.
|
||||
// * The IPv4 address. It is in the parcel, but FFWifiConnection has no field for it and
|
||||
// `inf.description` is the interface name, as on every other platform. The LocalIP module
|
||||
// reports addresses.
|
||||
// * `inf.description` while the interface is not in the address list. Its name is not knowable
|
||||
// from anywhere else -- sysfs and /proc/net are EACCES -- so the field stays empty rather than
|
||||
// naming an interface that was guessed at. `inf.status` is still reported, from the radio.
|
||||
|
||||
#define FF_WIFI_ANDROID_SERVICE "wifi"
|
||||
#define FF_WIFI_ANDROID_DESCRIPTOR "android.net.wifi.IWifiManager"
|
||||
|
||||
// Android 11 is the release that moved the Wi-Fi framework out of framework.jar and into an APEX,
|
||||
// which is where this jar is. Nothing older carries it, so there is no second path to try: the
|
||||
// class is either in the APEX or the release is too old.
|
||||
#define FF_WIFI_ANDROID_JAR "/apex/com.android.wifi/javalib/framework-wifi.jar"
|
||||
#define FF_WIFI_ANDROID_STUB "Landroid/net/wifi/IWifiManager$Stub;"
|
||||
#define FF_WIFI_ANDROID_GET_CONNECTION_INFO "TRANSACTION_getConnectionInfo"
|
||||
// Whether the radio is on is the one thing the connection does not say. An empty `WifiInfo` is what
|
||||
// the service answers with both while Wi-Fi is off and while it is on with nothing associated, so
|
||||
// the two states are told apart by asking the service directly. The method takes no arguments and
|
||||
// answers with a `WifiManager.WIFI_STATE_*`.
|
||||
#define FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE "TRANSACTION_getWifiEnabledState"
|
||||
#define FF_WIFI_ANDROID_WIFI_STATE_DISABLED 1
|
||||
#define FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN 4
|
||||
|
||||
// The other argument is the caller's package name. The shell UID owns exactly one package and the
|
||||
// service accepts that name, which is what `cmd` and `dumpsys` pass. See getOwnPackage().
|
||||
#define FF_WIFI_ANDROID_SHELL_UID 2000
|
||||
#define FF_WIFI_ANDROID_SHELL_PACKAGE "com.android.shell"
|
||||
|
||||
// An AIDL reply opens with the exception code and, for a Parcelable return, a non-null marker;
|
||||
// WifiInfo itself then starts at 8. The network id, the RSSI and the link speed have held those
|
||||
// offsets from Android 9 to 16, and the transmit speed joined them in Android 10 without moving
|
||||
// since, so all four are read at fixed offsets. Everything after them is located by shape.
|
||||
#define FF_WIFI_ANDROID_OFF_NET_ID 8
|
||||
#define FF_WIFI_ANDROID_OFF_RSSI 12
|
||||
#define FF_WIFI_ANDROID_OFF_LINK_SPEED 16
|
||||
#define FF_WIFI_ANDROID_OFF_TX_LINK_SPEED 20
|
||||
|
||||
// A `WifiInfo` with no connection to describe carries the sentinels for the two fields that are
|
||||
// always at a fixed offset: -1 for a network id it does not have and -127 for a signal it cannot
|
||||
// measure. Both were measured on the vivo's Android 16 reply, which is what the service answers
|
||||
// while Wi-Fi is on with nothing associated -- the head holds no frequency and no SSID at all, only
|
||||
// the "02:00:00:00:00:00" placeholder that `WifiInfo` writes in place of a BSSID.
|
||||
#define FF_WIFI_ANDROID_NET_ID_NONE (-1)
|
||||
#define FF_WIFI_ANDROID_RSSI_NONE (-127)
|
||||
|
||||
// Frequency is looked for in the head, after the link speeds and before the address. See the note
|
||||
// above for why it is not at a fixed offset.
|
||||
#define FF_WIFI_ANDROID_FREQ_SCAN_FROM 0x14
|
||||
#define FF_WIFI_ANDROID_FREQ_SCAN_TO 0x34
|
||||
|
||||
// Distances from Frequency. AOSP writes an address as a flag byte followed by a byte array, and
|
||||
// omits the array entirely when there is no IPv4 address -- an IPv6-only network does that -- which
|
||||
// shifts everything after it by two words. The base of the SSID section is therefore derived from
|
||||
// that flag rather than fixed, and only the SSID's own offset is left as a pair: Android 11 wrote
|
||||
// the length twice in front of the octets and Android 12 dropped the duplicate.
|
||||
//
|
||||
// The SSID is the one field WifiInfo writes as raw bytes rather than as a UTF-16 string, so its
|
||||
// length is a byte count and a NUL among the bytes means the offset was wrong.
|
||||
#define FF_WIFI_ANDROID_FREQ_HAS_IP 0x04
|
||||
#define FF_WIFI_ANDROID_FREQ_IP_LENGTH 0x08
|
||||
#define FF_WIFI_ANDROID_FREQ_SSID_BASE 0x10
|
||||
#define FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP 0x08
|
||||
#define FF_WIFI_ANDROID_SSID_LENGTH 0x04
|
||||
|
||||
// The receive speed is the word in front of Frequency; the transmit speed is the word after the link
|
||||
// speed. The vivo writes one extra word between those two that AOSP does not -- it repeats the
|
||||
// transmit speed -- so the receive speed can only be reached from Frequency, and Frequency cannot be
|
||||
// reached by stepping over the two link speeds. Both were checked against `cmd wifi status` at the
|
||||
// same moment: 131/219 on the vivo, 86/-1 on the Redmi.
|
||||
#define FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED 0x04
|
||||
#define FF_WIFI_ANDROID_SSID 0x08 // Android 12 and later
|
||||
#define FF_WIFI_ANDROID_SSID_LEGACY 0x0c // Android 11 and older
|
||||
#define FF_WIFI_ANDROID_SSID_MAX_LENGTH 32
|
||||
|
||||
// `WifiInfo` writes the Wi-Fi standard as one of the `ScanResult.WIFI_STANDARD_*` values. It is
|
||||
// followed by the two maximum-supported link speeds, which is what makes it recognisable: an int
|
||||
// naming a standard, then two plausible rates. The band is checked against it as well, so a value
|
||||
// that only fits another band is rejected. A layout that is not recognised leaves `protocol` empty
|
||||
// instead of reporting the wrong standard.
|
||||
#define FF_WIFI_ANDROID_STANDARD_LEGACY 1
|
||||
#define FF_WIFI_ANDROID_STANDARD_11A 2
|
||||
#define FF_WIFI_ANDROID_STANDARD_11B 3
|
||||
#define FF_WIFI_ANDROID_STANDARD_11N 4
|
||||
#define FF_WIFI_ANDROID_STANDARD_11AC 5
|
||||
#define FF_WIFI_ANDROID_STANDARD_11AX 6
|
||||
#define FF_WIFI_ANDROID_STANDARD_11AD 7
|
||||
#define FF_WIFI_ANDROID_STANDARD_11BE 8
|
||||
#define FF_WIFI_ANDROID_RATE_MAX 20000
|
||||
|
||||
// The whole WifiInfo parcel measured 1340 bytes on Android 16, most of it the MLO and ANQP tail.
|
||||
#define FF_WIFI_ANDROID_REPLY_SIZE 4096
|
||||
|
||||
// The layout is found by shape rather than by offset, so a rejected reply leaves nothing to go on.
|
||||
// The debug dump covers the head -- where the frequency, the address flag and the SSID sit -- and
|
||||
// stops before the tail, which is the long MLO and ANQP list and takes no part in locating
|
||||
// anything. Read with `fastfetch -s Wifi` under a debug build.
|
||||
#define FF_WIFI_ANDROID_DEBUG_DUMP_SIZE 0x60
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Binder
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// `getConnectionInfo` takes the caller's package name, and whether it is checked depends on the
|
||||
// release: the vivo answered "Package com.termux does not belong to 2000" for a name the shell does
|
||||
// not own, while the Redmi accepted the same name. Passing the real one is what works on both. There
|
||||
// is no way for a process to ask for its own package name -- it is not in /proc/self/status, and an
|
||||
// app cannot list /data/data -- but the executable path carries it: an app's binaries live under
|
||||
// /data/data/<package>/ or /data/user/<user>/<package>/, and /proc/self/exe resolves there.
|
||||
//
|
||||
// A binary outside those directories has no package of its own, and that is not an edge case: a
|
||||
// static build pushed to /data/local/tmp is how this runs on a device without Termux, and the Redmi
|
||||
// reported "Cannot determine the package name of this process" for exactly that. The shell UID can
|
||||
// be answered instead, because it has one name the service accepts. No other UID reaches here: an
|
||||
// app's own binaries are always under its data directory, so a failure there is a real failure.
|
||||
static bool getOwnPackage(char* buffer, size_t capacity) {
|
||||
char path[4096];
|
||||
const ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
if (length <= 0) {
|
||||
FF_DEBUG("Cannot read /proc/self/exe, so the calling package cannot be derived");
|
||||
return false;
|
||||
}
|
||||
path[length] = '\0';
|
||||
|
||||
const char* rest = nullptr;
|
||||
const char* const dataPrefix = "/data/data/";
|
||||
const char* const userPrefix = "/data/user/";
|
||||
if (strncmp(path, dataPrefix, strlen(dataPrefix)) == 0) {
|
||||
rest = path + strlen(dataPrefix);
|
||||
} else if (strncmp(path, userPrefix, strlen(userPrefix)) == 0) {
|
||||
rest = strchr(path + strlen(userPrefix), '/');
|
||||
if (rest != nullptr) {
|
||||
rest += 1;
|
||||
}
|
||||
}
|
||||
if (rest == nullptr) {
|
||||
FF_DEBUG("The executable is not in an app data directory (\"%s\"), uid %u", path, (unsigned) getuid());
|
||||
if (getuid() != FF_WIFI_ANDROID_SHELL_UID) {
|
||||
return false;
|
||||
}
|
||||
static const char shellPackage[] = FF_WIFI_ANDROID_SHELL_PACKAGE;
|
||||
if (sizeof(shellPackage) > capacity) {
|
||||
return false;
|
||||
}
|
||||
memcpy(buffer, shellPackage, sizeof(shellPackage));
|
||||
FF_DEBUG("The calling package is \"%s\" (the shell owns it)", buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* end = strchr(rest, '/');
|
||||
const size_t nameLength = end != nullptr ? (size_t) (end - rest) : strlen(rest);
|
||||
if (nameLength == 0 || nameLength >= capacity) {
|
||||
return false;
|
||||
}
|
||||
memcpy(buffer, rest, nameLength);
|
||||
buffer[nameLength] = '\0';
|
||||
FF_DEBUG("The calling package is \"%s\"", buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool isMacAddress(const char* value, uint32_t length) {
|
||||
if (length != 17) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < length; ++i) {
|
||||
const char c = value[i];
|
||||
if (i % 3 == 2) {
|
||||
if (c != ':') {
|
||||
return false;
|
||||
}
|
||||
} else if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// SupplicantState, in the order AOSP declares it.
|
||||
static const char* const FF_WIFI_ANDROID_STATES[] = {
|
||||
"DISCONNECTED", "INTERFACE_DISABLED", "INACTIVE", "SCANNING", "AUTHENTICATING", "ASSOCIATING",
|
||||
"ASSOCIATED", "FOUR_WAY_HANDSHAKE", "GROUP_HANDSHAKE", "COMPLETED", "DORMANT", "UNINITIALIZED", "INVALID",
|
||||
};
|
||||
|
||||
static bool isSupplicantState(const char* value, uint32_t length) {
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(FF_WIFI_ANDROID_STATES); ++i) {
|
||||
if (strlen(FF_WIFI_ANDROID_STATES[i]) == length && memcmp(FF_WIFI_ANDROID_STATES[i], value, length) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
typedef struct FFWifiAndroidConnection {
|
||||
char interface[IF_NAMESIZE + 1];
|
||||
char state[24];
|
||||
char ssid[FF_WIFI_ANDROID_SSID_MAX_LENGTH + 1];
|
||||
char bssid[18];
|
||||
int32_t rssi;
|
||||
int32_t linkSpeed;
|
||||
int32_t txLinkSpeed;
|
||||
int32_t rxLinkSpeed;
|
||||
int32_t standard;
|
||||
size_t stateEnd;
|
||||
uint16_t frequency;
|
||||
bool up;
|
||||
bool upKnown; // whether the state of the interface was established at all
|
||||
bool connected;
|
||||
} FFWifiAndroidConnection;
|
||||
|
||||
// The Wi-Fi interface is the one the HAL names `wlan*`, and it is looked up rather than assumed so
|
||||
// that a second one (`wlan1` on a dual-STA device) is still reported by name. The flags describe the
|
||||
// interface, so the first entry for a name is as good as any other.
|
||||
//
|
||||
// What bionic's `getifaddrs` will not do is list an interface that has no address: the list is
|
||||
// built out of the addresses, so `wlan0` leaves it as soon as nothing is associated -- measured on
|
||||
// the vivo, and the Redmi behaves the same. That is exactly the state its flags are wanted in, so
|
||||
// when the name is missing the radio state stands in for them. See detectWithBinder().
|
||||
static void detectInterface(FFWifiAndroidConnection* connection) {
|
||||
struct ifaddrs* addrs = nullptr;
|
||||
if (getifaddrs(&addrs) != 0) {
|
||||
FF_DEBUG("getifaddrs failed, so neither the interface name nor its state is known");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const struct ifaddrs* ifa = addrs; ifa != nullptr; ifa = ifa->ifa_next) {
|
||||
if (ifa->ifa_name == nullptr || strncmp(ifa->ifa_name, "wlan", 4) != 0) {
|
||||
continue;
|
||||
}
|
||||
snprintf(connection->interface, sizeof(connection->interface), "%s", ifa->ifa_name);
|
||||
connection->up = (ifa->ifa_flags & IFF_UP) != 0;
|
||||
connection->upKnown = true;
|
||||
FF_DEBUG("Interface \"%s\" is %s (IFF_UP %s)", connection->interface, connection->up ? "up" : "down", connection->up ? "set" : "clear");
|
||||
break;
|
||||
}
|
||||
freeifaddrs(addrs);
|
||||
if (connection->interface[0] == '\0') {
|
||||
FF_DEBUG("No interface named wlan* is present");
|
||||
}
|
||||
}
|
||||
|
||||
const char* ffDetectWifi(FFlist* result) {
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||
static const char* wifiStandardName(int32_t standard) {
|
||||
switch (standard) {
|
||||
case FF_WIFI_ANDROID_STANDARD_LEGACY: return "802.11";
|
||||
case FF_WIFI_ANDROID_STANDARD_11A: return "802.11a";
|
||||
case FF_WIFI_ANDROID_STANDARD_11B: return "802.11b";
|
||||
case FF_WIFI_ANDROID_STANDARD_11N: return "802.11n (Wi-Fi 4)";
|
||||
case FF_WIFI_ANDROID_STANDARD_11AC: return "802.11ac (Wi-Fi 5)";
|
||||
case FF_WIFI_ANDROID_STANDARD_11AX: return "802.11ax (Wi-Fi 6)";
|
||||
case FF_WIFI_ANDROID_STANDARD_11AD: return "802.11ad (WiGig)";
|
||||
case FF_WIFI_ANDROID_STANDARD_11BE: return "802.11be (Wi-Fi 7)";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (ffProcessAppendStdOut(&buffer, (char* const[]) { FF_TERMUX_API_PATH, FF_TERMUX_API_PARAM, nullptr })) {
|
||||
return "Starting `" FF_TERMUX_API_PATH " " FF_TERMUX_API_PARAM "` failed";
|
||||
// 11ac and 11ad live in bands the connection is not in when it reports a 2.4 GHz frequency, so a
|
||||
// candidate that disagrees with the band is a misread rather than a standard.
|
||||
static bool isStandardPlausible(int32_t standard, uint16_t frequency) {
|
||||
if (frequency < 3000 && (standard == FF_WIFI_ANDROID_STANDARD_11AC || standard == FF_WIFI_ANDROID_STANDARD_11AD)) {
|
||||
return false;
|
||||
}
|
||||
if (frequency > 50000 && standard != FF_WIFI_ANDROID_STANDARD_11AD) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// WifiInfo writes the BSSID, the MAC and the supplicant state as length-prefixed UTF-16 strings, and
|
||||
// their distance from the head depends on how long the SSID is and on which release wrote the
|
||||
// parcel. They are recognised by shape instead of by offset: a 17 character `hh:hh:hh:hh:hh:hh`, and
|
||||
// one of the thirteen state names. The BSSID is written before the MAC, so the first match is the
|
||||
// one wanted. `from` is the end of the SSID octets, which is where the BSSID follows.
|
||||
static void findStrings(const uint8_t* data, size_t size, size_t from, FFWifiAndroidConnection* connection) {
|
||||
for (size_t offset = from; offset + 4 <= size; ++offset) {
|
||||
const uint32_t length = ffBinderReadU32(data, size, offset);
|
||||
if (length == 0 || length > 24 || offset + 4 + (size_t) length * 2 > size) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t* chars = data + offset + 4;
|
||||
char buffer[25];
|
||||
bool ascii = true;
|
||||
for (uint32_t i = 0; i < length; ++i) {
|
||||
if (chars[i * 2 + 1] != 0 || chars[i * 2] < 0x20 || chars[i * 2] >= 0x7f) {
|
||||
ascii = false;
|
||||
break;
|
||||
}
|
||||
buffer[i] = (char) chars[i * 2];
|
||||
}
|
||||
if (!ascii) {
|
||||
continue;
|
||||
}
|
||||
buffer[length] = '\0';
|
||||
|
||||
if (connection->bssid[0] == '\0' && isMacAddress(buffer, length)) {
|
||||
memcpy(connection->bssid, buffer, length + 1);
|
||||
} else if (connection->state[0] == '\0' && isSupplicantState(buffer, length)) {
|
||||
memcpy(connection->state, buffer, length + 1);
|
||||
// The state is the last string before the tail, so where it ends is where the tail
|
||||
// starts. A Parcel pads a string to the next word, terminator included.
|
||||
connection->stateEnd = offset + 4 + ((length * 2 + 2 + 3) & ~3u);
|
||||
}
|
||||
if (connection->bssid[0] != '\0' && connection->state[0] != '\0') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AOSP writes the Wi-Fi standard between three nullable strings and the two maximum-supported link
|
||||
// speeds, all of them ints. Vendors insert fields around that run -- the Android 11 build on the
|
||||
// Redmi carries three words before the standard that AOSP's own Android 11 does not -- so a fixed
|
||||
// offset would be a guess. The run is recognised by shape instead, searching forward from the end of
|
||||
// the supplicant state: an int naming a standard, followed by two rates, in a band the standard can
|
||||
// live in.
|
||||
static void findWifiStandard(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) {
|
||||
if (connection->stateEnd == 0) {
|
||||
return; // without the state string there is no trustworthy place to start from
|
||||
}
|
||||
|
||||
[[gnu::cleanup(wrapYyjsonFree)]] yyjson_doc* doc = yyjson_read_opts(buffer.chars, buffer.length, 0, nullptr, nullptr);
|
||||
if (!doc) {
|
||||
return "Failed to parse wifi connection info";
|
||||
for (size_t offset = connection->stateEnd; offset + 12 <= size; offset += 4) {
|
||||
const int32_t standard = ffBinderReadI32(data, size, offset);
|
||||
if (wifiStandardName(standard) == nullptr || !isStandardPlausible(standard, connection->frequency)) {
|
||||
continue;
|
||||
}
|
||||
const int32_t maxTx = ffBinderReadI32(data, size, offset + 4);
|
||||
const int32_t maxRx = ffBinderReadI32(data, size, offset + 8);
|
||||
if (maxTx < 0 || maxTx > FF_WIFI_ANDROID_RATE_MAX || maxRx < 0 || maxRx > FF_WIFI_ANDROID_RATE_MAX) {
|
||||
continue;
|
||||
}
|
||||
connection->standard = standard;
|
||||
FF_DEBUG("Wi-Fi standard %d at +0x%zx, maximum speeds %d/%d", standard, offset, maxTx, maxRx);
|
||||
return;
|
||||
}
|
||||
FF_DEBUG("No Wi-Fi standard matched after the supplicant state at +0x%zx", connection->stateEnd);
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
// The reply is read by shape, so a reply that fails every shape leaves nothing behind to diagnose
|
||||
// it with: the bytes are the only evidence. Each line carries the words next to their bytes,
|
||||
// because what the reader is looking for -- a value that would pass for a frequency, an address
|
||||
// flag, a string length -- is an int.
|
||||
static void debugDumpReply(const uint8_t* data, size_t size) {
|
||||
const size_t limit = size < FF_WIFI_ANDROID_DEBUG_DUMP_SIZE ? size : FF_WIFI_ANDROID_DEBUG_DUMP_SIZE;
|
||||
for (size_t offset = 0; offset < limit; offset += 16) {
|
||||
char hex[16 * 3 + 1];
|
||||
size_t used = 0;
|
||||
for (size_t i = 0; i < 16 && offset + i < limit; ++i) {
|
||||
used += (size_t) snprintf(hex + used, sizeof(hex) - used, "%02x ", data[offset + i]);
|
||||
}
|
||||
FF_DEBUG(" +0x%02zx %-47s | %d %d %d %d", offset, hex,
|
||||
ffBinderReadI32(data, size, offset), ffBinderReadI32(data, size, offset + 4),
|
||||
ffBinderReadI32(data, size, offset + 8), ffBinderReadI32(data, size, offset + 12));
|
||||
}
|
||||
}
|
||||
#define FF_WIFI_ANDROID_DEBUG_DUMP(data, size) debugDumpReply(data, size)
|
||||
#else
|
||||
#define FF_WIFI_ANDROID_DEBUG_DUMP(data, size) ((void) 0)
|
||||
#endif
|
||||
|
||||
static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) {
|
||||
if (size < FF_WIFI_ANDROID_OFF_TX_LINK_SPEED + 4) {
|
||||
FF_DEBUG("The reply is %zu bytes, too short to hold a WifiInfo", size);
|
||||
return "The reply is too short to hold a WifiInfo";
|
||||
}
|
||||
const int32_t exception = ffBinderReadI32(data, size, 0);
|
||||
if (exception != 0) {
|
||||
FF_DEBUG("The Wifi service raised exception %d", exception);
|
||||
return "The Wifi service raised an exception";
|
||||
}
|
||||
if (ffBinderReadU32(data, size, 4) == 0) {
|
||||
// A null WifiInfo is what the service returns when it has nothing to report, which is the
|
||||
// normal reply while Wi-Fi is off. The interface, when it is still there, is reported as
|
||||
// disconnected.
|
||||
FF_DEBUG("The reply is %zu bytes and carries no WifiInfo", size);
|
||||
return nullptr;
|
||||
}
|
||||
FF_DEBUG("Reply is %zu bytes: netId %d, rssi %d, link speed %d, tx link speed %d",
|
||||
size,
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID),
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI),
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED),
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_TX_LINK_SPEED));
|
||||
|
||||
// The two orders WifiSsid has written its length in: once before the octets on Android 11 and
|
||||
// older, not at all on Android 12 and later, which leaves the octets one word earlier. The one
|
||||
// that matches this release is tried first.
|
||||
uint32_t ssidOffsets[2] = { FF_WIFI_ANDROID_SSID_LEGACY, FF_WIFI_ANDROID_SSID };
|
||||
if (FF_API_AT_LEAST(31)) {
|
||||
ssidOffsets[0] = FF_WIFI_ANDROID_SSID;
|
||||
ssidOffsets[1] = FF_WIFI_ANDROID_SSID_LEGACY;
|
||||
}
|
||||
|
||||
yyjson_val* root = yyjson_doc_get_root(doc);
|
||||
if (!yyjson_is_obj(root)) {
|
||||
return "Wifi info result is not a JSON object";
|
||||
for (uint32_t base = FF_WIFI_ANDROID_FREQ_SCAN_FROM; base <= FF_WIFI_ANDROID_FREQ_SCAN_TO; base += 4) {
|
||||
const uint16_t frequency = (uint16_t) ffBinderReadU32(data, size, base);
|
||||
if (frequency == 0 || ffWifiFreqToChannel(frequency) == 0) {
|
||||
continue;
|
||||
}
|
||||
FF_DEBUG("+0x%02x holds %u, which is channel %u", base, frequency, ffWifiFreqToChannel(frequency));
|
||||
|
||||
const int32_t hasIp = ffBinderReadI32(data, size, base + FF_WIFI_ANDROID_FREQ_HAS_IP);
|
||||
if (hasIp != 0 && hasIp != 1) {
|
||||
FF_DEBUG(" rejected: +0x%02x is not an address flag but %d", base + FF_WIFI_ANDROID_FREQ_HAS_IP, hasIp);
|
||||
continue;
|
||||
}
|
||||
const uint32_t ipLength = ffBinderReadU32(data, size, base + FF_WIFI_ANDROID_FREQ_IP_LENGTH);
|
||||
if (hasIp == 1 && ipLength != 4) {
|
||||
FF_DEBUG(" rejected: the address at +0x%02x is %u bytes long", base + FF_WIFI_ANDROID_FREQ_IP_LENGTH, ipLength);
|
||||
continue;
|
||||
}
|
||||
const uint32_t ssidBase = base + (hasIp == 1 ? FF_WIFI_ANDROID_FREQ_SSID_BASE : FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP);
|
||||
const int32_t ssidPresent = ffBinderReadI32(data, size, ssidBase);
|
||||
if (ssidPresent != 1) {
|
||||
FF_DEBUG(" rejected: the SSID at +0x%02x is marked %d, not present", ssidBase, ssidPresent);
|
||||
continue; // the SSID of a reported connection is never null
|
||||
}
|
||||
|
||||
const uint32_t ssidLength = ffBinderReadU32(data, size, ssidBase + FF_WIFI_ANDROID_SSID_LENGTH);
|
||||
if (ssidLength == 0 || ssidLength > FF_WIFI_ANDROID_SSID_MAX_LENGTH) {
|
||||
FF_DEBUG(" rejected: the SSID length at +0x%02x is %u", ssidBase + FF_WIFI_ANDROID_SSID_LENGTH, ssidLength);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(ssidOffsets); ++i) {
|
||||
const size_t ssidOffset = ssidBase + ssidOffsets[i];
|
||||
if (ssidOffset + ssidLength > size) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The SSID is a raw byte string, so a NUL in it means this was not the SSID after all.
|
||||
if (memchr(data + ssidOffset, '\0', ssidLength) != nullptr) {
|
||||
FF_DEBUG(" rejected: the %u SSID bytes at +0x%zx hold a NUL", ssidLength, ssidOffset);
|
||||
continue;
|
||||
}
|
||||
memcpy(connection->ssid, data + ssidOffset, ssidLength);
|
||||
connection->ssid[ssidLength] = '\0';
|
||||
|
||||
connection->frequency = frequency;
|
||||
connection->rssi = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI);
|
||||
connection->linkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED);
|
||||
connection->txLinkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_TX_LINK_SPEED);
|
||||
connection->rxLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED);
|
||||
|
||||
findStrings(data, size, ssidOffset + ssidLength, connection);
|
||||
FF_DEBUG("SSID \"%s\" at +0x%zx, BSSID \"%s\", supplicant state \"%s\"",
|
||||
connection->ssid, ssidOffset, connection->bssid, connection->state);
|
||||
// The supplicant state is the authoritative signal; the network id is the fallback for a
|
||||
// parcel whose state string was not recognised.
|
||||
connection->connected = connection->state[0] != '\0'
|
||||
? strcmp(connection->state, "COMPLETED") == 0
|
||||
: ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID) >= 0;
|
||||
findWifiStandard(data, size, connection);
|
||||
FF_DEBUG("Standard %d (%s), rx link speed %d, connected %s",
|
||||
connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->rxLinkSpeed,
|
||||
connection->connected ? "yes" : "no");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing in the head matched, but an unassociated connection has nothing in the head to match:
|
||||
// no frequency, no SSID and -- unlike a connection -- the two sentinels `WifiInfo` fills in when
|
||||
// it has nothing to describe. That reply is the service saying there is nothing to report, which
|
||||
// is a state and not a failure. A reply carrying a network id of its own is a connection, so a
|
||||
// shape that still does not match one keeps being reported as a failure.
|
||||
if (ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID) == FF_WIFI_ANDROID_NET_ID_NONE &&
|
||||
ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI) == FF_WIFI_ANDROID_RSSI_NONE) {
|
||||
FF_DEBUG("No network id and no signal in the %zu byte reply: there is no connection to report", size);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FF_DEBUG("No frequency, no SSID and no supplicant state matched in this reply:");
|
||||
FF_WIFI_ANDROID_DEBUG_DUMP(data, size);
|
||||
return "The Wifi service returned a layout fastfetch does not understand";
|
||||
}
|
||||
|
||||
// Which of the speeds the service wrote becomes a rate. It writes a negative one when it does not
|
||||
// know, which is what the Redmi does for the receive side, and a link speed of 0 when the link is
|
||||
// down. `generic` is the fallback for a direction the service left unset: the transmit side has one
|
||||
// -- the generic link speed is a real negotiated rate, just not direction-specific -- the way the
|
||||
// Linux backend falls back to SIOCGIWRATE when the station info carries no bitrate. There is no
|
||||
// generic receive speed, so 0 is passed there.
|
||||
static double wifiAndroidRate(int32_t specific, int32_t generic) {
|
||||
const int32_t rate = specific > 0 ? specific : generic;
|
||||
return rate > 0 && rate <= FF_WIFI_ANDROID_RATE_MAX ? (double) rate : -DBL_MAX;
|
||||
}
|
||||
|
||||
// Calls a method of the service that takes no arguments and answers with an int. The transaction
|
||||
// code comes out of the jar the same way `getConnectionInfo`'s does, so a release that does not
|
||||
// declare the method answers UNKNOWN_TRANSACTION instead of a number that means something else.
|
||||
static const char* callIntMethod(FFBinder* binder, uint32_t handle, const char* transactionField, int32_t* result) {
|
||||
int32_t transaction = 0;
|
||||
const char* error = ffDexStaticInt(FF_WIFI_ANDROID_JAR, FF_WIFI_ANDROID_STUB, transactionField, &transaction);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
|
||||
uint8_t parcelBuffer[256];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_WIFI_ANDROID_DESCRIPTOR);
|
||||
|
||||
uint8_t replyBuffer[64];
|
||||
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
|
||||
error = ffBinderTransact(binder, handle, (uint32_t) transaction, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
if (ffBinderReplyIsStatus(&reply)) {
|
||||
FF_DEBUG("\"%s\" resolved to transaction %d, which this build does not answer", transactionField, transaction);
|
||||
return "The Wifi service does not answer that request";
|
||||
}
|
||||
const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
|
||||
if (exception != 0) {
|
||||
FF_DEBUG("\"%s\" is transaction %d and raised exception %d", transactionField, transaction, exception);
|
||||
return "The Wifi service raised an exception";
|
||||
}
|
||||
*result = ffBinderReadI32(reply.data, reply.size, 4);
|
||||
FF_DEBUG("\"%s\" is transaction %d and answered %d", transactionField, transaction, *result);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const char* detectWithBinder(FFlist* result) {
|
||||
char package[128];
|
||||
if (!getOwnPackage(package, sizeof(package))) {
|
||||
return "Cannot determine the package name of this process";
|
||||
}
|
||||
|
||||
int32_t transaction = 0;
|
||||
const char* error = ffDexStaticInt(FF_WIFI_ANDROID_JAR, FF_WIFI_ANDROID_STUB, FF_WIFI_ANDROID_GET_CONNECTION_INFO, &transaction);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Reading the transaction code from \"%s\" failed: %s", FF_WIFI_ANDROID_JAR, error);
|
||||
return error;
|
||||
}
|
||||
|
||||
[[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
|
||||
error = ffBinderOpen(&binder);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
|
||||
uint32_t handle = 0;
|
||||
error = ffBinderLookupService(&binder, FF_WIFI_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &handle);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
FF_DEBUG("The \"%s\" service is handle %u, %s is transaction %d",
|
||||
FF_WIFI_ANDROID_SERVICE, handle, FF_WIFI_ANDROID_GET_CONNECTION_INFO, transaction);
|
||||
|
||||
uint8_t parcelBuffer[256];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_WIFI_ANDROID_DESCRIPTOR);
|
||||
ffBinderParcelPutString16(&parcel, package);
|
||||
// callingFeatureId is nullable, and a null String is a length of -1 rather than an empty string.
|
||||
ffBinderParcelPutI32(&parcel, -1);
|
||||
|
||||
uint8_t replyBuffer[FF_WIFI_ANDROID_REPLY_SIZE];
|
||||
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
|
||||
error = ffBinderTransact(&binder, handle, (uint32_t) transaction, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
if (ffBinderReplyIsStatus(&reply)) {
|
||||
FF_DEBUG("The reply carries status %d instead of a parcel", (int) ffBinderReadI32(reply.data, reply.size, 0));
|
||||
return "Wifi service rejected the request";
|
||||
}
|
||||
|
||||
FFWifiAndroidConnection connection = {};
|
||||
detectInterface(&connection);
|
||||
error = parseConnectionInfo(reply.data, reply.size, &connection);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!connection.upKnown) {
|
||||
// `getifaddrs` leaves the interface out while it has no address, so its flags are missing in
|
||||
// exactly the state they are wanted in. The radio answers for it: a Wi-Fi that is off is one
|
||||
// whose interface is down, and a Wi-Fi that is on with nothing associated keeps an interface
|
||||
// that is up. WIFI_STATE_DISABLING is a radio that is still up, so only the one value means
|
||||
// down, and WIFI_STATE_UNKNOWN is the service declining to say.
|
||||
int32_t state = 0;
|
||||
const char* stateError = callIntMethod(&binder, handle, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, &state);
|
||||
if (stateError == nullptr && state != FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN) {
|
||||
connection.up = state != FF_WIFI_ANDROID_WIFI_STATE_DISABLED;
|
||||
connection.upKnown = true;
|
||||
FF_DEBUG("The radio is %s, which stands in for the interface flags", connection.up ? "on" : "off");
|
||||
} else {
|
||||
FF_DEBUG("The state of the radio is not known: %s",
|
||||
stateError != nullptr ? stateError : "the service answered WIFI_STATE_UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing was found to report on at all. That is a device without a Wi-Fi interface rather than
|
||||
// one with a Wi-Fi that is off, which has a state to print by now.
|
||||
if (!connection.upKnown && !connection.connected) {
|
||||
return "No Wi-Fi interface is present";
|
||||
}
|
||||
|
||||
FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result);
|
||||
@@ -45,26 +675,40 @@ const char* ffDetectWifi(FFlist* result) {
|
||||
item->conn.channelWidth = 0;
|
||||
item->conn.frequency = 0;
|
||||
|
||||
ffStrbufAppendJsonVal(&item->inf.status, yyjson_obj_get(root, "supplicant_state"));
|
||||
if (!item->inf.status.length) {
|
||||
ffStrbufAppendS(&item->inf.status, "Unknown");
|
||||
ffStrbufSetS(&item->inf.description, connection.interface);
|
||||
// The name is empty when the interface was not in the address list, and the state is empty when
|
||||
// nothing established it: both are left empty rather than guessed at. The module prints this
|
||||
// state whenever there is no SSID to print instead, which is exactly the case where neither is
|
||||
// in the reply.
|
||||
if (connection.upKnown) {
|
||||
ffStrbufSetStatic(&item->inf.status, connection.up ? "Up" : "Down");
|
||||
}
|
||||
ffStrbufSetStatic(&item->conn.status, connection.connected ? "connected" : "disconnected");
|
||||
if (!connection.connected) {
|
||||
FF_DEBUG("Nothing is associated: interface \"%s\" is \"%s\", the connection is \"%s\"",
|
||||
connection.interface, item->inf.status.chars, item->conn.status.chars);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!ffStrbufEqualS(&item->inf.status, "COMPLETED")) {
|
||||
return nullptr;
|
||||
const char* protocol = wifiStandardName(connection.standard);
|
||||
if (protocol != nullptr) {
|
||||
ffStrbufSetStatic(&item->conn.protocol, protocol);
|
||||
}
|
||||
|
||||
double rssi = yyjson_get_num(yyjson_obj_get(root, "rssi"));
|
||||
item->conn.signalQuality = rssi >= -50 ? 100 : rssi <= -100 ? 0
|
||||
: (rssi + 100) * 2;
|
||||
|
||||
ffStrbufAppendJsonVal(&item->inf.description, yyjson_obj_get(root, "ip"));
|
||||
ffStrbufAppendJsonVal(&item->conn.bssid, yyjson_obj_get(root, "bssid"));
|
||||
ffStrbufAppendJsonVal(&item->conn.ssid, yyjson_obj_get(root, "ssid"));
|
||||
item->conn.frequency = (uint16_t) yyjson_get_int(yyjson_obj_get(root, "frequency_mhz"));
|
||||
item->conn.txRate = yyjson_get_num(yyjson_obj_get(root, "link_speed_mbps"));
|
||||
item->conn.channel = ffWifiFreqToChannel(item->conn.frequency);
|
||||
|
||||
item->conn.signalQuality = connection.rssi >= -50 ? 100 : connection.rssi <= -100 ? 0
|
||||
: (connection.rssi + 100) * 2;
|
||||
ffStrbufSetS(&item->conn.bssid, connection.bssid);
|
||||
ffStrbufSetS(&item->conn.ssid, connection.ssid);
|
||||
item->conn.frequency = connection.frequency;
|
||||
item->conn.txRate = wifiAndroidRate(connection.txLinkSpeed, connection.linkSpeed);
|
||||
item->conn.rxRate = wifiAndroidRate(connection.rxLinkSpeed, 0);
|
||||
item->conn.channel = ffWifiFreqToChannel(connection.frequency);
|
||||
FF_DEBUG("\"%s\" %s: \"%s\", \"%s\", %s, %u MHz (channel %u), signal %.0f, tx %d, rx %d",
|
||||
connection.interface, item->inf.status.chars, item->conn.ssid.chars, item->conn.bssid.chars,
|
||||
item->conn.protocol.length ? item->conn.protocol.chars : "(no standard)", connection.frequency,
|
||||
item->conn.channel, item->conn.signalQuality, connection.txLinkSpeed, connection.rxLinkSpeed);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* ffDetectWifi(FFlist* result) {
|
||||
return detectWithBinder(result);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user