diff --git a/CMakeLists.txt b/CMakeLists.txt index d615923de..44426df45 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -707,6 +707,7 @@ elseif(ANDROID) src/detection/poweradapter/poweradapter_nosupport.c src/detection/processes/processes_linux.c src/detection/sound/sound_android.c + src/detection/sound/sound_linux.c src/detection/swap/swap_linux.c src/detection/terminalfont/terminalfont_android.c src/detection/terminalfont/terminalfont_linux.c diff --git a/src/common/android/dex.c b/src/common/android/dex.c index 4926e3338..c1de5b6fe 100644 --- a/src/common/android/dex.c +++ b/src/common/android/dex.c @@ -1,4 +1,6 @@ #include "common/android/dex.h" +#include "common/debug.h" +#include "common/mallocHelper.h" #include #include @@ -228,17 +230,18 @@ static const char* dexString(const uint8_t* dex, const uint8_t* end, uint32_t in 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. -// -// `found` separates "this dex does not define the class" from a real failure, because a jar spreads -// its classes over several dex files and the caller has to try the next one. It is set before any -// of the walks below, so an error after it still means the class was here. -static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* classDescriptor, const char* fieldName, bool* found, int32_t* result) { - *found = false; - const uint8_t* end = dex + size; +// One dex entry, validated. Every offset in the header comes out of the file, so each table is range +// checked before the walk uses it. +typedef struct FFDexTables { + const uint8_t* dex; + const uint8_t* end; + const uint8_t* types; + uint32_t typeCount; + const uint8_t* classes; + uint32_t classCount; +} FFDexTables; +static const char* dexOpen(const uint8_t* dex, size_t size, FFDexTables* tables) { if (size < FF_DEX_HEADER_SIZE || dexU32(dex + FF_DEX_OFF_ENDIAN_TAG) != FF_DEX_ENDIAN_TAG) { return "Not a dex file"; } @@ -246,26 +249,13 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla if (fileSize > size || fileSize < FF_DEX_HEADER_SIZE) { return "The dex file size is out of range"; } - end = dex + fileSize; + const uint8_t* 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 (!dexTableInRange(dex, end, types, 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 nullptr; - } - *found = true; const uint8_t* classes = dex + dexU32(dex + FF_DEX_OFF_CLASS_DEFS); const uint32_t classCount = dexU32(dex + FF_DEX_OFF_CLASS_DEFS_SIZE); @@ -273,11 +263,75 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla 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; + tables->dex = dex; + tables->end = end; + tables->types = types; + tables->typeCount = typeCount; + tables->classes = classes; + tables->classCount = classCount; + return nullptr; +} + +// One pass over the type table, resolving the type index of every class that still has a request +// open. This pass is what a lookup costs: `framework.jar`'s classes.dex lists 8428 descriptors, each +// one a `string_data_item` read from a different part of a 51 MB mapping. Matching every request +// against the same pass is what makes a batch cheaper than the sum of its fields -- the descriptor is +// read once however many classes are being looked for, and the pass ends as soon as the last request +// has an index rather than at the end of the table. +// +// `typeIndexes` is parallel to `requests` and rebuilt here rather than kept across entries: a type +// index is a property of the entry's own table, and the next entry numbers its types differently. +static void dexFindTypes(const FFDexTables* tables, const FFDexStaticIntRequest* requests, const uint8_t* settled, uint32_t* typeIndexes, uint32_t count) { + uint32_t remaining = 0; + for (uint32_t r = 0; r < count; ++r) { + typeIndexes[r] = UINT32_MAX; + if (!settled[r]) { + ++remaining; + } + } + + for (uint32_t i = 0; i < tables->typeCount && remaining > 0; ++i) { + const char* descriptor = dexString(tables->dex, tables->end, dexU32(tables->types + (size_t) i * 4)); + if (descriptor == nullptr) { + continue; + } + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] != UINT32_MAX || strcmp(descriptor, requests[r].classDescriptor) != 0) { + continue; + } + typeIndexes[r] = i; + --remaining; + } + } +} + +// Walks the static field list of the class that `typeIndex` names and the class's static value array +// in step, writing the value of every request for that class whose field name matches. 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 written. +// +// `defined` says whether this dex *defines* the class, which is not the same as mentioning it: a dex +// lists every type it references in its type table, so a class that a later entry of the jar defines +// still has a type index here, with no class_def_item behind it. Only a class_def_item makes the +// answer this dex's; without one the caller moves on to the next entry. +// +// A request this class cannot answer -- a field it does not declare, or one it declares as something +// other than an int -- is closed here rather than reported: the class is this dex's, so no later +// entry of the jar could give it a different answer, and one field the caller can live without must +// not hold back the rest of the batch. It is closed with the sentinel, and what this returns is only +// ever a dex that cannot be read, which is the one thing no later entry can fix. +static const char* dexClassStaticInts(const FFDexTables* tables, uint32_t typeIndex, const FFDexStaticIntRequest* requests, const uint32_t* typeIndexes, uint8_t* settled, uint32_t count, bool* defined) { + *defined = false; + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + + for (uint32_t i = 0; i < tables->classCount; ++i) { + const uint8_t* classDef = tables->classes + (size_t) i * FF_DEX_CLASS_DEF_SIZE; if (dexU32(classDef) != typeIndex) { continue; } + // Defined here, so from this point on the answer is this dex's, whatever it turns out to be. + *defined = true; 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); @@ -319,20 +373,40 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla 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) { + if (name == nullptr) { + continue; + } + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] != typeIndex || strcmp(name, requests[r].fieldName) != 0) { + continue; + } if (!isInt) { // The field exists but is not an int, so its value is not the constant asked - // for. Reported here rather than on the first non-int in the list, because the - // list legitimately holds strings (`DESCRIPTOR`) and booleans. - return "The dex static field is not an int"; + // for. Closed the way a field the class does not declare is, because it is this + // dex's answer too. The check is here rather than on the first non-int in the + // list because the list legitimately holds strings (`DESCRIPTOR`) and booleans. + *requests[r].result = FF_DEX_STATIC_INT_UNRESOLVED; + settled[r] = 1; + continue; } - *result = value; - return nullptr; + *requests[r].result = value; + settled[r] = 1; } } - return "The field is not a static field of the class"; + + // The class is defined here, so a request of its own that went unanswered is one whose field + // the class does not declare as a static field. Closed here for the same reason as above. + for (uint32_t r = 0; r < count; ++r) { + if (!settled[r] && typeIndexes[r] == typeIndex) { + *requests[r].result = FF_DEX_STATIC_INT_UNRESOLVED; + settled[r] = 1; + } + } + return nullptr; } - return "The class has no class_def_item"; + // The descriptor is in this dex's type table but the class itself is defined by another entry of + // the jar. That is not a failure: `defined` stays false and the caller reads the next entry. + return nullptr; } // --------------------------------------------------------------------------------------------- @@ -340,16 +414,14 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla // --------------------------------------------------------------------------------------------- typedef struct FFDexMapping { - uint8_t* mapped; // the jar, as mapped; nullptr once released + uint8_t* mapped; // the jar, as mapped; nullptr once released size_t mappedSize; - const uint8_t* data; // the dex bytes: inside `mapped`, or `inflated` + const uint8_t* data; // the dex bytes, inside `mapped` 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); } @@ -469,7 +541,9 @@ static const char* inflateDex(const uint8_t* data, size_t dataSize, uint32_t unc FF_LIBRARY_LOAD_SYMBOL(zlib, inflate, "dlsym(inflate) failed") FF_LIBRARY_LOAD_SYMBOL(zlib, inflateEnd, "dlsym(inflateEnd) failed") - uint8_t* buffer = malloc(uncompressedSize); + // Released by the cleanup on every path out below; ownership moves to the caller by clearing + // the pointer, which is also why no failure path has to free it by hand. + FF_AUTO_FREE uint8_t* buffer = malloc(uncompressedSize); if (buffer == nullptr) { return "malloc failed"; } @@ -484,22 +558,31 @@ static const char* inflateDex(const uint8_t* data, size_t dataSize, uint32_t unc 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"; + return "Inflating the dex failed"; } *out = buffer; + buffer = nullptr; // the caller owns it from here return nullptr; } #endif -const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, const char* fieldName, int32_t* result) { +const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count) { + // Every request is written the sentinel before anything can fail, so that a caller reads a result + // rather than whatever its own stack held there, however this returns. Nothing below has to + // remember to do it on its way out. + for (uint32_t r = 0; r < count; ++r) { + *requests[r].result = FF_DEX_STATIC_INT_UNRESOLVED; + } + if (count == 0) { + return nullptr; + } + [[gnu::cleanup(wrapDexMapping)]] FFDexMapping mapping = {}; const int fd = open(jarPath, O_RDONLY | O_CLOEXEC); @@ -521,14 +604,28 @@ const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, con return "mmap(jar) failed"; } + // One byte per request saying whether it is closed -- answered, or given up on -- and the type + // index it was found under in the entry being walked. Both are sized from `count` rather than + // from a fixed bound, and both are released by their cleanup however this function returns. + FF_AUTO_FREE uint8_t* settled = calloc(count, 1); + FF_AUTO_FREE uint32_t* typeIndexes = malloc((size_t) count * sizeof(uint32_t)); + if (settled == nullptr || typeIndexes == nullptr) { + return "malloc failed"; + } + // A jar spreads its classes over `classes.dex`, `classes2.dex`, `classes3.dex`, ... and the // class being looked for is in whichever one the build put it in: `IAudioService$Stub` is in the // second dex of framework.jar, while `IWifiManager$Stub` is in the first of framework-wifi.jar. // The entries are therefore tried in order, and the first one that defines the class answers. // The count is a bound on the walk, not a claim about the jar -- AOSP has never shipped more - // than a handful, and a class that is in none of them is reported as missing either way. - const char* error = "The class is not in the jar"; - for (uint32_t index = 1; index <= FF_DEX_MAX_ENTRIES; ++index) { + // than a handful, and a class that is in none of them is left unanswered either way. + // + // The walk ends as soon as the last request is closed, so an entry that holds every class asked + // for -- which is what the modules here ask for, one class per interface -- costs one entry read + // and one pass over its type table. + uint32_t pending = count; + const char* error = nullptr; + for (uint32_t index = 1; index <= FF_DEX_MAX_ENTRIES && pending > 0; ++index) { char entry[sizeof("classes999.dex")]; if (index == 1) { memcpy(entry, FF_DEX_ENTRY, sizeof(FF_DEX_ENTRY)); @@ -540,6 +637,10 @@ const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, con size_t dataSize = 0; uint32_t uncompressedSize = 0; uint16_t method = FF_ZIP_METHOD_STORED; + // The decompressed dex, when the entry needed one. The cleanup releases it at the end of + // the iteration, so every entry gets a buffer of its own and no way out of the loop -- an + // early return included -- can leak one. + FF_AUTO_FREE uint8_t* inflated = nullptr; if (findDexEntry(mapping.mapped, mapping.mappedSize, entry, &data, &dataSize, &uncompressedSize, &method) != nullptr) { if (index > 1) { @@ -553,14 +654,11 @@ const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, con } } else if (method == FF_ZIP_METHOD_DEFLATED) { #ifdef FF_HAVE_ZLIB - // One buffer is reused across the walk, so the previous dex is released first. - free(mapping.inflated); - mapping.inflated = nullptr; - error = inflateDex(data, dataSize, uncompressedSize, &mapping.inflated); + error = inflateDex(data, dataSize, uncompressedSize, &inflated); if (error != nullptr) { return error; } - data = mapping.inflated; + data = inflated; dataSize = uncompressedSize; #else return "The jar deflates its dex entries and fastfetch was built without zlib"; @@ -577,15 +675,58 @@ const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, con mapping.data = data; mapping.size = dataSize; - bool found = false; - error = dexStaticInt(mapping.data, mapping.size, classDescriptor, fieldName, &found, result); + FFDexTables tables = {}; + error = dexOpen(mapping.data, mapping.size, &tables); if (error != nullptr) { + // The dex cannot be read at all, which no later entry can fix. return error; } - if (found) { - return nullptr; + + dexFindTypes(&tables, requests, settled, typeIndexes, count); + + // Every class this entry defines is walked once, however many of its fields were asked for. + // A request whose class is not in this entry's type table keeps `UINT32_MAX` and is left for + // the next entry; one whose class is only referenced here is left the same way by the walk + // below, which is what `defined` reports. + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] == UINT32_MAX) { + continue; + } + const uint32_t typeIndex = typeIndexes[r]; + bool defined = false; + error = dexClassStaticInts(&tables, typeIndex, requests, typeIndexes, settled, count, &defined); + if (error != nullptr) { + // The dex cannot be read, which no later entry can fix. + return error; + } + if (!defined) { + // Only referenced here, so the class is left for the next entry -- and the requests + // that share it are marked as walked, since one lookup of a class answers all of + // them and the class table should not be scanned once per field. + for (uint32_t s = r; s < count; ++s) { + if (typeIndexes[s] == typeIndex) { + typeIndexes[s] = UINT32_MAX; + } + } + } + } + + pending = 0; + for (uint32_t r = 0; r < count; ++r) { + if (!settled[r]) { + ++pending; + } } - error = "The class is not in the jar"; } - return error; + + // What is still open is a class no entry of the jar defines. That is an answer like any other -- + // the results already hold the sentinel, written before the walk began -- so the only thing left + // is to say so for a debug build, where the sentinel on its own does not tell "this build has no + // such method" apart from "the reader went wrong". + for (uint32_t r = 0; r < count; ++r) { + if (*requests[r].result == FF_DEX_STATIC_INT_UNRESOLVED) { + FF_DEBUG("\"%s\" is not a static int field of \"%s\"", requests[r].fieldName, requests[r].classDescriptor); + } + } + return nullptr; } diff --git a/src/common/android/dex.h b/src/common/android/dex.h index 4c44724f8..6c324d8c3 100644 --- a/src/common/android/dex.h +++ b/src/common/android/dex.h @@ -24,8 +24,41 @@ #include "fastfetch.h" -// Resolves the int value of `.` out of the dex entries of `jarPath`, -// walking `classes.dex`, `classes2.dex`, ... in order until one of them defines the class. -// `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); +// What a request is written when the jar does not answer it, on every path out of ffDexStaticInts -- +// a jar that cannot be opened and a field the class does not declare both leave it behind, so a +// caller reads a result rather than what its own stack happened to hold there. No transaction code +// is negative: AIDL numbers them from `IBinder.FIRST_CALL_TRANSACTION`, which is 1. +// +// This is what keeps the reader a lookup and nothing more. Whether a code is one the caller cannot do +// without or one it only uses when the build declares it, whether what came back is enough to carry +// on, and whether the module reports a value or an error are the caller's decisions, made by reading +// the results. The reader finds numbers and says which ones it could not. +#define FF_DEX_STATIC_INT_UNRESOLVED (-1) + +// One `.` lookup, and where its value is written. `classDescriptor` is +// the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;". +typedef struct FFDexStaticIntRequest { + const char* classDescriptor; + const char* fieldName; + int32_t* result; +} FFDexStaticIntRequest; + +// Resolves the int value of every request out of the dex entries of `jarPath`, walking `classes.dex`, +// `classes2.dex`, ... in order until one of them defines the class. +// +// Asking for several fields at once is not a convenience. A lookup is dominated by the type table, +// which lists every type the dex references -- 8428 descriptors in `framework.jar`'s `classes.dex`, +// each one read from a different part of a 51 MB mapping -- and a single pass answers every request +// whose class is named in it. Measured over three devices' own `framework.jar`, four fields of two +// classes cost one pass rather than four. +// +// Every request is written a code or FF_DEX_STATIC_INT_UNRESOLVED. A request is never allowed to +// take the others down with it: the classes a caller asks for are looked for independently, and one +// the jar does not declare leaves its own result at the sentinel while the rest are answered. +// +// Not finding a code is not a failure. A jar that is readable answers the call, whether it declares +// every field asked for, some of them, or none -- the results say which, and what that means for the +// module is the caller's to decide. The return value is therefore reserved for the one thing the +// caller cannot read out of the results: the jar itself, which is reported when it cannot be opened +// or does not hold a dex that can be read. nullptr means the lookup ran. +[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count); diff --git a/src/detection/battery/battery_android.c b/src/detection/battery/battery_android.c index 369e1372a..348b7aadd 100644 --- a/src/detection/battery/battery_android.c +++ b/src/detection/battery/battery_android.c @@ -2,6 +2,7 @@ #include "battery.h" #include "common/android/api.h" #include "common/android/binder.h" +#include "common/android/dex.h" #include "common/debug.h" #include "common/processing.h" #include "common/properties.h" @@ -18,19 +19,31 @@ // everything the module would otherwise like to show. Manufacturer, model name, technology, serial // number, manufacture date, temperature and cycle count all sit behind that permission. -// The transaction code and the reply layout are both positional, and neither is negotiated with the -// service, so each only stays correct as long as AOSP keeps the order it already has. The code has -// not: `IBatteryPropertiesRegistrar` still declared registerListener / unregisterListener / -// getProperty up to Android 9, and Android 10 dropped the two listener methods, which moved -// `getProperty` from the third position to the first. The layout has, because `BatteryProperty` has -// only ever grown at the end -- API 35 appended [string8 mValueString] to it -- so the field read -// below is still the first one written. +// The reply layout is positional and is not negotiated with the service, so it only stays correct as +// long as AOSP keeps the order it already has. It has, because `BatteryProperty` has only ever grown +// at the end -- API 35 appended [string8 mValueString] to it -- so the field read below is still the +// first one written. +// +// The transaction code is positional for the same reason, and it has *not* stayed: the interface +// still declared registerListener / unregisterListener / getProperty up to Android 9, and Android 10 +// dropped the two listener methods, which moved `getProperty` from the third position to the first. +// The number is therefore read out of the device's own jar rather than guessed from the API level, +// because a vendor fork is free to insert a method ahead of it -- and a wrong code reaches a +// different method, or none. See common/android/dex.h. // // Getting either wrong is quiet rather than loud: `ffBinderReadU64` returns 0 for a read past the // end of the reply, so a drift shows up as a wrong capacity, not as a failed call. #define FF_BATTERY_ANDROID_SERVICE "batteryproperties" #define FF_BATTERY_ANDROID_DESCRIPTOR "android.os.IBatteryPropertiesRegistrar" +// IBatteryPropertiesRegistrar has always been part of framework.jar -- it is not one of the +// frameworks that moved into an APEX -- so there is one path and no fallback. The method this module +// calls is named by the constant the dex carries for it, which is what the lookup turns into whatever +// number this build uses. +#define FF_BATTERY_ANDROID_JAR "/system/framework/framework.jar" +#define FF_BATTERY_ANDROID_STUB "Landroid/os/IBatteryPropertiesRegistrar$Stub;" +#define FF_BATTERY_ANDROID_GET_PROPERTY "TRANSACTION_getProperty" + // BatteryManager.BATTERY_PROPERTY_* typedef enum FFBatteryAndroidProperty : uint32_t { FF_BATTERY_ANDROID_PROPERTY_CAPACITY = 4, @@ -103,15 +116,8 @@ static void debugDumpReply(const uint8_t* data, size_t size) { #define FF_BATTERY_ANDROID_DEBUG_DUMP(data, size) ((void) 0) #endif -static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t property, uint64_t* value) { - // `getProperty` is the third method declared in `IBatteryPropertiesRegistrar` up to Android 9 and - // the first one from Android 10 on. Written as an `if` because clang rejects `__builtin_available` - // -- which is what FF_ANDROID_API_AT_LEAST expands to -- in any other position. - uint32_t transaction = 3u; - if (FF_ANDROID_API_AT_LEAST(29)) { - transaction = 1u; - } - FF_DEBUG("Property %u goes out as transaction %u (3 up to Android 9, 1 from Android 10)", property, transaction); +static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t transaction, uint32_t property, uint64_t* value) { + FF_DEBUG("Property %u goes out as transaction %u", property, transaction); uint8_t parcelBuffer[128]; FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); @@ -165,8 +171,23 @@ static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t prope } static const char* parseBinder(FFlist* results) { + // Resolved before the binder is opened, so that a jar which does not declare the method says so + // rather than the module failing later with a message about the transport. The reader reports a + // code it cannot find by leaving the sentinel in the result and says nothing more -- what a + // missing code means for this module is decided here, not there. + int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED; + const FFDexStaticIntRequest request = { FF_BATTERY_ANDROID_STUB, FF_BATTERY_ANDROID_GET_PROPERTY, &transaction }; + const char* error = ffDexStaticInts(FF_BATTERY_ANDROID_JAR, &request, 1); + if (error != nullptr) { + FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", FF_BATTERY_ANDROID_GET_PROPERTY, FF_BATTERY_ANDROID_JAR, error); + return error; + } + if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) { + return "The battery service does not declare getProperty"; + } + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; - const char* error = ffBinderOpen(&binder); + error = ffBinderOpen(&binder); if (error != nullptr) { FF_DEBUG("Opening /dev/binder failed: %s", error); return error; @@ -179,17 +200,17 @@ static const char* parseBinder(FFlist* results) { FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_BATTERY_ANDROID_SERVICE, error); return error; } - FF_DEBUG("The \"%s\" service is handle %u", FF_BATTERY_ANDROID_SERVICE, service.handle); + FF_DEBUG("The \"%s\" service is handle %u, getProperty is transaction %d", FF_BATTERY_ANDROID_SERVICE, service.handle, transaction); uint64_t capacity = 0; - error = getProperty(&binder, service.handle, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity); + error = getProperty(&binder, service.handle, (uint32_t) transaction, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity); if (error != nullptr) { FF_DEBUG("The capacity is what failed, so no battery is reported at all"); return error; } uint64_t status = 0; - error = getProperty(&binder, service.handle, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status); + error = getProperty(&binder, service.handle, (uint32_t) transaction, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status); if (error != nullptr) { FF_DEBUG("The status is what failed, so no battery is reported at all"); return error; diff --git a/src/detection/media/media_android.c b/src/detection/media/media_android.c index 53596e760..bebe25664 100644 --- a/src/detection/media/media_android.c +++ b/src/detection/media/media_android.c @@ -1,6 +1,7 @@ #include "media.h" #include "common/android/api.h" #include "common/android/binder.h" +#include "common/android/dex.h" #include "common/debug.h" #include "common/io.h" #include "common/mallocHelper.h" @@ -70,15 +71,23 @@ #define FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR "android.media.session.ISessionManager" #define FF_MEDIA_ANDROID_CONTROLLER_DESCRIPTOR "android.media.session.ISessionController" -// Transaction codes as this device's own framework.jar declares them, read out of its dex rather than -// transcribed from AOSP -- a vendor ROM adds and drops methods as it likes, and a wrong code reaches -// a different method or none at all. getSessions is the second declared method of ISessionManager; -// the numbers on ISessionController are sparse because most of that interface is setters this module -// never calls. -#define FF_MEDIA_ANDROID_GET_SESSIONS 2u -#define FF_MEDIA_ANDROID_GET_PACKAGE_NAME 5u -#define FF_MEDIA_ANDROID_GET_METADATA 32u -#define FF_MEDIA_ANDROID_GET_PLAYBACK_STATE 33u +// The jar and the stub classes the transaction codes are read out of. Both interfaces have always +// been part of framework.jar -- neither is one of the frameworks that moved into an APEX -- so there +// is one path and no fallback. +#define FF_MEDIA_ANDROID_JAR "/system/framework/framework.jar" +#define FF_MEDIA_ANDROID_SESSIONS_STUB "Landroid/media/session/ISessionManager$Stub;" +#define FF_MEDIA_ANDROID_CONTROLLER_STUB "Landroid/media/session/ISessionController$Stub;" + +// The methods this module calls, by the name of the constant the dex carries for each. Spelling the +// names out rather than the numbers is the whole point: the numbering is a property of the `.aidl` +// the ROM was built from, and a vendor that inserts a method ahead of one shifts it, after which a +// transcribed code reaches a different method or none. `getSessions` is 2 from Android 12 on but 3 on +// Android 11, where `notifySession2Created` holds the second slot -- which is exactly the drift a +// hardcoded number cannot survive. See common/android/dex.h. +#define FF_MEDIA_ANDROID_GET_SESSIONS "TRANSACTION_getSessions" +#define FF_MEDIA_ANDROID_GET_PACKAGE_NAME "TRANSACTION_getPackageName" +#define FF_MEDIA_ANDROID_GET_METADATA "TRANSACTION_getMetadata" +#define FF_MEDIA_ANDROID_GET_PLAYBACK_STATE "TRANSACTION_getPlaybackState" // The largest reply any of these getters produced is a metadata Bundle at ~1.6 KB, and the artwork // never lands here -- it arrives as a descriptor. 16 KB leaves room for a Bundle of unusually long @@ -447,6 +456,43 @@ static bool androidCompressCover(FFNativeFD fd, const void* pixels, uint32_t wid // The session // --------------------------------------------------------------------------------------------- +// Every transaction code this module sends, resolved from the device's own jar. They are resolved +// together, in one walk, because that is the cheap way to get them: the walk is dominated by the +// dex's type table, and the two stub classes sit in the same entry of every framework.jar seen so far +// (classes2.dex on Android 11 and 13, classes3.dex on Android 16), so one pass answers all four. +// Asking one at a time would repeat that pass four times -- see common/android/dex.h. +typedef struct FFMediaAndroidCodes { + int32_t getSessions; + int32_t getPackageName; + int32_t getMetadata; + int32_t getPlaybackState; +} FFMediaAndroidCodes; + +static const char* androidResolveCodes(FFMediaAndroidCodes* codes) { + const FFDexStaticIntRequest requests[] = { + { FF_MEDIA_ANDROID_SESSIONS_STUB, FF_MEDIA_ANDROID_GET_SESSIONS, &codes->getSessions }, + { FF_MEDIA_ANDROID_CONTROLLER_STUB, FF_MEDIA_ANDROID_GET_PACKAGE_NAME, &codes->getPackageName }, + { FF_MEDIA_ANDROID_CONTROLLER_STUB, FF_MEDIA_ANDROID_GET_METADATA, &codes->getMetadata }, + { FF_MEDIA_ANDROID_CONTROLLER_STUB, FF_MEDIA_ANDROID_GET_PLAYBACK_STATE, &codes->getPlaybackState }, + }; + + const char* error = ffDexStaticInts(FF_MEDIA_ANDROID_JAR, requests, ARRAY_SIZE(requests)); + if (error != nullptr) { + FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_MEDIA_ANDROID_JAR, error); + return error; + } + + // All four are codes the module cannot answer without. The reader leaves a code it could not find + // at the sentinel instead of judging what that costs the caller, so the judgement is made here. + if (codes->getSessions == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getPackageName == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getMetadata == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getPlaybackState == FF_DEX_STATIC_INT_UNRESOLVED) { + return "The session service does not declare every method this module calls"; + } + return nullptr; +} + // Every getter on ISessionController takes no argument, so a call is the interface token and nothing // else; `flags` is passed through because one of them needs TF_ACCEPT_FDS. [[gnu::nonnull(1, 5)]] static const char* androidCall(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, FFBinderReply* reply) { @@ -730,9 +776,9 @@ static int64_t androidElapsedRealtime(void) { return nullptr; } -static void androidReadPackageName(FFBinder* binder, uint32_t token, FFMediaResult* result) { +static void androidReadPackageName(FFBinder* binder, uint32_t token, uint32_t transaction, FFMediaResult* result) { FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); - const char* error = androidCall(binder, token, FF_MEDIA_ANDROID_GET_PACKAGE_NAME, 0, &reply); + const char* error = androidCall(binder, token, transaction, 0, &reply); if (error != nullptr) { FF_DEBUG("getPackageName failed: %s", error); return; @@ -751,14 +797,14 @@ static void androidReadPackageName(FFBinder* binder, uint32_t token, FFMediaResu ffStrbufSet(&result->player, &result->playerId); } -static void androidReadMetadata(FFBinder* binder, uint32_t token, FFMediaResult* result, bool saveCover) { +static void androidReadMetadata(FFBinder* binder, uint32_t token, uint32_t transaction, FFMediaResult* result, bool saveCover) { FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); // TF_ACCEPT_FDS is not a hint and not optional here: the artwork arrives as a descriptor, and a // request that did not ask for one is answered with BR_FAILED_REPLY -- which reads like a service // that is not running rather than like a missing flag. It is passed unconditionally, because the // alternative is a failure that depends on whether the player happened to publish artwork. - const char* error = androidCall(binder, token, FF_MEDIA_ANDROID_GET_METADATA, TF_ACCEPT_FDS, &reply); + const char* error = androidCall(binder, token, transaction, TF_ACCEPT_FDS, &reply); FFMediaAndroidCover cover = { .fd = -1 }; if (error != nullptr) { @@ -782,14 +828,14 @@ static void androidReadMetadata(FFBinder* binder, uint32_t token, FFMediaResult* } } -static void androidReadSession(FFBinder* binder, uint32_t token, FFMediaResult* result, bool saveCover) { - androidReadPackageName(binder, token, result); +static void androidReadSession(FFBinder* binder, uint32_t token, const FFMediaAndroidCodes* codes, FFMediaResult* result, bool saveCover) { + androidReadPackageName(binder, token, (uint32_t) codes->getPackageName, result); // The length has to be known before the position is placed, so the metadata comes first. - androidReadMetadata(binder, token, result, saveCover); + androidReadMetadata(binder, token, (uint32_t) codes->getMetadata, result, saveCover); FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); - const char* error = androidCall(binder, token, FF_MEDIA_ANDROID_GET_PLAYBACK_STATE, 0, &reply); + const char* error = androidCall(binder, token, (uint32_t) codes->getPlaybackState, 0, &reply); if (error == nullptr && ffBinderReadI32(reply.data, reply.size, 4) != 0) { FFMediaAndroidReader reader = { .data = reply.data, .size = reply.size, .position = 8 }; error = androidReadPlaybackState(&reader, result); @@ -810,8 +856,18 @@ static void androidReadSession(FFBinder* binder, uint32_t token, FFMediaResult* // answer. That costs one transaction, and it keeps the module working wherever the check happens to // pass, instead of hard-coding today's policy into the client. static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCover) { + // Resolved before the binder is opened, so that a jar which does not declare the methods says so + // rather than the module failing later with a message about the transport. The reader's own + // reason is what is returned: a jar that cannot be read, a class it does not define and a field + // the class does not declare are three different things. + FFMediaAndroidCodes codes = {}; + const char* error = androidResolveCodes(&codes); + if (error != nullptr) { + return error; + } + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; - const char* error = ffBinderOpen(&binder); + error = ffBinderOpen(&binder); if (error != nullptr) { return error; } @@ -822,6 +878,7 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove FF_DEBUG("Looking up \"%s\" failed: %s", FF_MEDIA_ANDROID_SERVICE, error); return error; } + FF_DEBUG("The \"%s\" service is handle %u, getSessions is transaction %d", FF_MEDIA_ANDROID_SERVICE, service.handle, codes.getSessions); // A phone can have several sessions at once -- a music player and a video one, say -- and only one // of them is the answer. Which one is decided first, by state alone, so that the metadata and the @@ -846,7 +903,7 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove ffBinderParcelPutI32(&parcel, 0); // userId: the single user a phone has FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); - error = ffBinderTransact(&binder, service.handle, FF_MEDIA_ANDROID_GET_SESSIONS, 0, &parcel, &reply); + error = ffBinderTransact(&binder, service.handle, (uint32_t) codes.getSessions, 0, &parcel, &reply); replied = true; if (error != nullptr) { return error; @@ -892,7 +949,7 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove uint32_t bestScore = 0; for (uint32_t i = 0; i < tokenCount; i++) { FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply)); - if (androidCall(&binder, tokens[i], FF_MEDIA_ANDROID_GET_PLAYBACK_STATE, 0, &reply) != nullptr) { + if (androidCall(&binder, tokens[i], (uint32_t) codes.getPlaybackState, 0, &reply) != nullptr) { continue; } // The presence marker first: a session with no state yet answers a null parcelable, and its @@ -905,7 +962,7 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove } } - androidReadSession(&binder, tokens[best], media, saveCover); + androidReadSession(&binder, tokens[best], &codes, media, saveCover); // The tokens were acquired by ffBinderTransact() and are given back by the cleanup attribute above. return nullptr; diff --git a/src/detection/sound/sound_android.c b/src/detection/sound/sound_android.c index 116295c95..78517f020 100644 --- a/src/detection/sound/sound_android.c +++ b/src/detection/sound/sound_android.c @@ -8,11 +8,11 @@ #include -// Android has no desktop sound stack. PulseAudio is not part of the platform -- a Termux user can -// start one, and then the module reads that, but its sink is a virtual device of Termux's own that -// feeds Android's OpenSL ES output with a volume that is always at full scale, so the number says -// nothing about the phone. ALSA and OSS are not there either: /dev/snd, /proc/asound and -// /sys/class/sound are all EACCES from an app's SELinux domain, `dumpsys audio` needs +// Android has no desktop sound stack. PulseAudio is not part of the platform, but a Termux user can +// run one, and that is what a session sounds through whenever the display server is a real desktop +// rather than SurfaceFlinger -- so this file answers for SurfaceFlinger alone and hands the rest to +// sound_linux.c, which speaks PulseAudio. ALSA and OSS are not there either: /dev/snd, /proc/asound +// and /sys/class/sound are all EACCES from an app's SELinux domain, `dumpsys audio` needs // android.permission.DUMP, and `settings get system volume_music` needs INTERACT_ACROSS_USERS. // `cmd audio` is worse than useless: `cmd` hands the arguments to the service over binder and the // command body runs inside system_server, so from an app it hangs or answers ENOSYS. @@ -214,15 +214,65 @@ static bool soundAndroidIsBluetooth(int32_t type) { } } -// Calls a method that takes no argument or one int, and answers with one int. Every transaction code -// is resolved here rather than passed in, so that the dex is the only place a number lives. -static const char* soundAndroidCallInt(FFBinder* binder, uint32_t handle, const char* transactionField, bool hasArgument, int32_t argument, int32_t* result) { - int32_t transaction = 0; - const char* error = ffDexStaticInt(FF_SOUND_ANDROID_JAR, FF_SOUND_ANDROID_STUB, transactionField, &transaction); +// Every transaction code this module sends, resolved from the device's own jar in one walk. They are +// asked for together because the walk is dominated by the dex's type table and all seven sit in the +// same class of the same entry -- see common/android/dex.h. +typedef struct FFSoundAndroidCodes { + int32_t getStreamVolume; + int32_t getStreamMinVolume; + int32_t getStreamMaxVolume; + int32_t isStreamMute; + int32_t getDevicesForAttributes; + int32_t isMusicActive; + int32_t getBtActiveDeviceName; +} FFSoundAndroidCodes; + +// The five codes the module cannot answer without, against the two that only refine the answer. A +// build that does not declare one of these five has nothing to report, which is what the module did +// when each code was resolved on its own. +static bool soundAndroidCodesIncomplete(const FFSoundAndroidCodes* codes) { + return codes->getStreamVolume == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getStreamMinVolume == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getStreamMaxVolume == FF_DEX_STATIC_INT_UNRESOLVED + || codes->isStreamMute == FF_DEX_STATIC_INT_UNRESOLVED + || codes->getDevicesForAttributes == FF_DEX_STATIC_INT_UNRESOLVED; +} + +// Resolves all seven in one call, and then makes the judgement the reader leaves to its caller. The +// two that only refine the answer are allowed to come back as the sentinel, because a build may +// declare one of them and not the other -- Mi 10 has `getDevicesForAttributesUnprotected` and no +// `getBtActiveDeviceName` -- and a code the module can live without must not take it down with it. +static const char* soundAndroidResolveCodes(FFSoundAndroidCodes* codes) { + const FFDexStaticIntRequest requests[] = { + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_STREAM_VOLUME, &codes->getStreamVolume }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_STREAM_MIN_VOLUME, &codes->getStreamMinVolume }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_STREAM_MAX_VOLUME, &codes->getStreamMaxVolume }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_IS_STREAM_MUTE, &codes->isStreamMute }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_DEVICES_FOR_ATTRIBUTES, &codes->getDevicesForAttributes }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_IS_MUSIC_ACTIVE, &codes->isMusicActive }, + { FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, &codes->getBtActiveDeviceName }, + }; + + const char* error = ffDexStaticInts(FF_SOUND_ANDROID_JAR, requests, ARRAY_SIZE(requests)); if (error != nullptr) { - FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", transactionField, FF_SOUND_ANDROID_JAR, error); + FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_SOUND_ANDROID_JAR, error); return error; } + if (soundAndroidCodesIncomplete(codes)) { + return "The audio service does not declare every method this module calls"; + } + return nullptr; +} + +// Calls a method that takes no argument or one int, and answers with one int. `transaction` comes out +// of the jar, and the sentinel the reader leaves for a code it could not find is caught here, so that +// a build which does not declare the method is a message rather than a transaction that means +// something else. `transactionField` is carried only to name the method in the debug output, which a +// release build compiles out -- hence the attribute. +static const char* soundAndroidCallInt(FFBinder* binder, uint32_t handle, [[maybe_unused]] const char* transactionField, int32_t transaction, bool hasArgument, int32_t argument, int32_t* result) { + if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) { + return "The audio service does not declare that method"; + } uint8_t parcelBuffer[FF_SOUND_ANDROID_SCALAR_PARCEL_SIZE]; FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); @@ -233,7 +283,7 @@ static const char* soundAndroidCallInt(FFBinder* binder, uint32_t handle, const uint8_t replyBuffer[FF_SOUND_ANDROID_SCALAR_REPLY_SIZE]; FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); - error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); + const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); if (error != nullptr) { return error; } @@ -322,12 +372,9 @@ static bool soundAndroidReadString16(const uint8_t* data, size_t size, size_t of // Bluetooth route as a whole and reports it even when the policy has selected the speaker, so it is // only ever consulted after the device list has already picked a Bluetooth sink, and only its name // is taken from it. -static const char* soundAndroidCallBtName(FFBinder* binder, uint32_t handle, FFstrbuf* result) { - int32_t transaction = 0; - const char* error = ffDexStaticInt(FF_SOUND_ANDROID_JAR, FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, &transaction); - if (error != nullptr) { - FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, FF_SOUND_ANDROID_JAR, error); - return error; +static const char* soundAndroidCallBtName(FFBinder* binder, uint32_t handle, int32_t transaction, FFstrbuf* result) { + if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) { + return "The audio service does not declare getBtActiveDeviceName"; } uint8_t parcelBuffer[FF_SOUND_ANDROID_SCALAR_PARCEL_SIZE]; @@ -336,7 +383,7 @@ static const char* soundAndroidCallBtName(FFBinder* binder, uint32_t handle, FFs uint8_t replyBuffer[FF_SOUND_ANDROID_NAME_REPLY_SIZE]; FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); - error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); + const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); if (error != nullptr) { return error; } @@ -425,13 +472,15 @@ static uint8_t soundAndroidPercent(int32_t volume, int32_t minimum, int32_t maxi } static const char* detectNative(FFSoundOptions* options, FFlist* devices) { - // Resolved before anything else so that a build without the method says so, rather than opening - // binder and failing later with a message about the transport. - int32_t transaction = 0; - const char* error = ffDexStaticInt(FF_SOUND_ANDROID_JAR, FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_DEVICES_FOR_ATTRIBUTES, &transaction); + // Resolved before anything else so that a build without the methods says so, rather than opening + // binder and failing later with a message about the transport. All seven codes are asked for in + // one call and judged here: the five the module cannot answer without decide whether it runs at + // all, and the two it only uses when the build declares them are left as sentinels for their + // call sites to cope with. + FFSoundAndroidCodes codes = {}; + const char* error = soundAndroidResolveCodes(&codes); if (error != nullptr) { - FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", FF_SOUND_ANDROID_GET_DEVICES_FOR_ATTRIBUTES, FF_SOUND_ANDROID_JAR, error); - return "The audio service does not declare getDevicesForAttributesUnprotected"; + return error; } [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; @@ -447,7 +496,7 @@ static const char* detectNative(FFSoundOptions* options, FFlist* devices) { return error; } FF_DEBUG("The \"%s\" service is handle %u, getDevicesForAttributesUnprotected is transaction %d", - FF_SOUND_ANDROID_SERVICE, service.handle, transaction); + FF_SOUND_ANDROID_SERVICE, service.handle, codes.getDevicesForAttributes); uint8_t parcelBuffer[FF_SOUND_ANDROID_DEVICE_PARCEL_SIZE]; FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); @@ -469,7 +518,7 @@ static const char* detectNative(FFSoundOptions* options, FFlist* devices) { uint8_t replyBuffer[FF_SOUND_ANDROID_DEVICE_REPLY_SIZE]; FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); - error = ffBinderTransact(&binder, service.handle, (uint32_t) transaction, 0, &parcel, &reply); + error = ffBinderTransact(&binder, service.handle, (uint32_t) codes.getDevicesForAttributes, 0, &parcel, &reply); if (error != nullptr) { return error; } @@ -531,15 +580,15 @@ static const char* detectNative(FFSoundOptions* options, FFlist* devices) { int32_t minimum = 0; int32_t maximum = 0; int32_t mute = 0; - error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_VOLUME, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &volume); + error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_VOLUME, codes.getStreamVolume, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &volume); if (error == nullptr) { - error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_MIN_VOLUME, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &minimum); + error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_MIN_VOLUME, codes.getStreamMinVolume, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &minimum); } if (error == nullptr) { - error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_MAX_VOLUME, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &maximum); + error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_MAX_VOLUME, codes.getStreamMaxVolume, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &maximum); } if (error == nullptr) { - error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_IS_STREAM_MUTE, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &mute); + error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_IS_STREAM_MUTE, codes.isStreamMute, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &mute); } if (error != nullptr) { return error; @@ -555,7 +604,7 @@ static const char* detectNative(FFSoundOptions* options, FFlist* devices) { // the ACTIVE bit to the device the policy already selected. A build that does not declare the // method, or a service that refuses it, leaves the bit clear rather than failing the module. int32_t active = 0; - const char* activeError = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_IS_MUSIC_ACTIVE, true, 0, &active); + const char* activeError = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_IS_MUSIC_ACTIVE, codes.isMusicActive, true, 0, &active); if (activeError != nullptr) { FF_DEBUG("Whether anything is playing is not known: %s", activeError); active = 0; @@ -575,7 +624,7 @@ static const char* detectNative(FFSoundOptions* options, FFlist* devices) { // in the type name, which is what this module printed before it asked at all. FF_STRBUF_AUTO_DESTROY bluetoothName = ffStrbufCreate(); if (soundAndroidIsBluetooth(deviceType)) { - const char* nameError = soundAndroidCallBtName(&binder, service.handle, &bluetoothName); + const char* nameError = soundAndroidCallBtName(&binder, service.handle, codes.getBtActiveDeviceName, &bluetoothName); if (nameError != nullptr || bluetoothName.length == 0) { FF_DEBUG("The Bluetooth device is not named: %s", nameError != nullptr ? nameError : "the service answered an empty name"); @@ -601,8 +650,17 @@ static const char* detectNative(FFSoundOptions* options, FFlist* devices) { } const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { + // The binder route below asks the policy about Android's own audio, which is the audio that is + // audible only while SurfaceFlinger is the display server. A Termux:X11 or Wayland session runs + // a real desktop on top of the device, and the sound there belongs to that desktop, not to + // Android -- so that case is handed to the Linux implementation, which speaks PulseAudio. Sound + // is only reported for one of the two, never merged. const FFDisplayServerResult* wm = ffConnectDisplayServer(); if (!ffStrbufIgnCaseEqualS(&wm->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER)) { - return nullptr; + FF_DEBUG("The display server is \"%s\", so the Linux implementation answers", wm->wmProtocolName.chars); + const char* ffDetectSoundLinux(FFSoundOptions* options, FFlist* devices); + return ffDetectSoundLinux(options, devices); } + + return detectNative(options, devices); } diff --git a/src/detection/sound/sound_linux.c b/src/detection/sound/sound_linux.c index eef2731e4..a615ee983 100644 --- a/src/detection/sound/sound_linux.c +++ b/src/detection/sound/sound_linux.c @@ -1076,7 +1076,17 @@ static const char* fetchSinkList(FFSoundPulseConn* conn, const FFSoundOptions* o // --------------------------------------------------------------------------- -const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { +// Android compiles this file next to sound_android.c, which owns ffDetectSound and calls this one +// only while the display server is not SurfaceFlinger -- a Termux:X11 or Wayland session runs a real +// desktop on top of the device, and PulseAudio is the sound of that desktop. So the entry point has +// to step aside there. Same arrangement as wallpaper_linux.c and terminalfont_linux.c. +const char* +#ifdef __ANDROID__ +ffDetectSoundLinux +#else +ffDetectSound +#endif + (FFSoundOptions* options, FFlist* devices) { FF_STRBUF_AUTO_DESTROY defaultSocketPath = ffStrbufCreate(); getDefaultSocketPath(&defaultSocketPath); FF_DEBUG("Default pulseaudio socket path: %s", defaultSocketPath.chars); diff --git a/src/detection/wallpaper/wallpaper_android.c b/src/detection/wallpaper/wallpaper_android.c index 33735a643..163d76130 100644 --- a/src/detection/wallpaper/wallpaper_android.c +++ b/src/detection/wallpaper/wallpaper_android.c @@ -1,5 +1,6 @@ #include "wallpaper.h" #include "common/android/binder.h" +#include "common/android/dex.h" #include "common/android/package.h" #include "common/debug.h" #include "common/io.h" @@ -44,10 +45,20 @@ typedef enum FFWallpaperAndroidWhich : int32_t { FF_WALLPAPER_ANDROID_WHICH_LOCK = 2, } FFWallpaperAndroidWhich; -// `getWallpaper` is the fourth method of IWallpaperManager$Stub, and the number is read from the -// device's own framework.jar rather than transcribed from AOSP, because a vendor ROM adds and drops -// methods as it likes -- this one dropped `getName`, whose code answers UNKNOWN_TRANSACTION here. -#define FF_WALLPAPER_ANDROID_TRANSACTION_GET_WALLPAPER 4u +// The jar and the stub class the transaction code is read out of. framework.jar answered on every +// device measured (Android 11, 13 and 16), so that is the one path and there is no fallback; the +// stub sits in classes.dex there, which is the entry the lookup tries first. +#define FF_WALLPAPER_ANDROID_JAR "/system/framework/framework.jar" +#define FF_WALLPAPER_ANDROID_STUB "Landroid/app/IWallpaperManager$Stub;" + +// The method this module calls, by the name of the constant the dex carries for it. `getWallpaper` +// is the fourth method of IWallpaperManager$Stub, but the number is resolved from the device's own +// jar rather than written down here: the numbering is a property of the `.aidl` the ROM was built +// from, and a vendor that inserts a method ahead of this one shifts it, after which a transcribed +// code reaches a different method or none. Spelling the name out is the whole point -- the lookup +// turns it into whatever this build uses, and a build that does not declare it says so instead of +// reaching another method. See common/android/dex.h. +#define FF_WALLPAPER_ANDROID_GET_WALLPAPER "TRANSACTION_getWallpaper" // `ffAndroidGetOwnPackage` writes at most this many bytes, terminator included. #define FF_WALLPAPER_ANDROID_PACKAGE_SIZE 128 @@ -67,7 +78,7 @@ typedef enum FFWallpaperAndroidWhich : int32_t { // being wrong here would fail the detection, not misread it. #define FF_WALLPAPER_ANDROID_REPLY_SIZE 512 -static const char* getWallpaperFile(FFBinder* binder, uint32_t handle, const char* package, FFWallpaperAndroidWhich which, FFstrbuf* result) { +static const char* getWallpaperFile(FFBinder* binder, uint32_t handle, const char* package, FFWallpaperAndroidWhich which, int32_t transaction, FFstrbuf* result) { uint8_t parcelBuffer[FF_WALLPAPER_ANDROID_PARCEL_SIZE]; FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); ffBinderParcelPutInterfaceToken(&parcel, FF_WALLPAPER_ANDROID_DESCRIPTOR); @@ -81,7 +92,7 @@ static const char* getWallpaperFile(FFBinder* binder, uint32_t handle, const cha // TF_ACCEPT_FDS is neither optional nor a hint: a reply carrying a descriptor is only delivered // when the request asked for one, and a request that did not is answered with BR_FAILED_REPLY -- // which reads like a service that is not running rather than like a missing flag. - const char* error = ffBinderTransact(binder, handle, FF_WALLPAPER_ANDROID_TRANSACTION_GET_WALLPAPER, TF_ACCEPT_FDS, &parcel, &reply); + const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, TF_ACCEPT_FDS, &parcel, &reply); // Whatever else happened, a descriptor that did arrive is ours: the kernel installed it in our fd // table, and on the paths below that return early nothing else would close it. @@ -147,8 +158,23 @@ const char* ffDetectWallpaper(FFstrbuf* result) { return "Cannot determine the package name of this process"; } + // Resolved before the binder is opened, so that a jar which does not declare the method says so + // rather than the module failing later with a message about the transport. The reader reports a + // code it cannot find by leaving the sentinel in the result and says nothing more -- what a + // missing code means for this module is decided here, not there. + int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED; + const FFDexStaticIntRequest request = { FF_WALLPAPER_ANDROID_STUB, FF_WALLPAPER_ANDROID_GET_WALLPAPER, &transaction }; + const char* error = ffDexStaticInts(FF_WALLPAPER_ANDROID_JAR, &request, 1); + if (error != nullptr) { + FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", FF_WALLPAPER_ANDROID_GET_WALLPAPER, FF_WALLPAPER_ANDROID_JAR, error); + return error; + } + if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) { + return "The wallpaper service does not declare getWallpaper"; + } + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; - const char* error = ffBinderOpen(&binder); + error = ffBinderOpen(&binder); if (error != nullptr) { return error; } @@ -160,16 +186,16 @@ const char* ffDetectWallpaper(FFstrbuf* result) { FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_WALLPAPER_ANDROID_SERVICE, error); return error; } - FF_DEBUG("The \"%s\" service is handle %u", FF_WALLPAPER_ANDROID_SERVICE, service.handle); + FF_DEBUG("The \"%s\" service is handle %u, getWallpaper is transaction %d", FF_WALLPAPER_ANDROID_SERVICE, service.handle, transaction); // The system wallpaper is what the module means by "the current wallpaper"; the lock screen is a // second, separate picture, and is only reported when the system one is not a file -- which is // the live-wallpaper case, where a component draws it and there is no image behind it. - error = getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_SYSTEM, result); + error = getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_SYSTEM, transaction, result); if (error == nullptr) { return nullptr; } FF_DEBUG("The system wallpaper is not a file (%s), so the lock screen is asked for instead", error); - return getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_LOCK, result); + return getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_LOCK, transaction, result); } diff --git a/src/detection/wifi/wifi_android.c b/src/detection/wifi/wifi_android.c index 0da49ad94..cbb4d37e1 100644 --- a/src/detection/wifi/wifi_android.c +++ b/src/detection/wifi/wifi_android.c @@ -827,15 +827,44 @@ static const char* wifiName(const char* value, bool withheld) { return withheld ? FF_WIFI_ANDROID_REDACTED : ""; } -// 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); +// Every transaction code this module sends, resolved from the device's own jar in one walk. They are +// asked for together because the walk is dominated by the dex's type table and both codes sit in the +// same class of the same entry -- see common/android/dex.h. +typedef struct FFWifiAndroidCodes { + int32_t getConnectionInfo; + int32_t getWifiEnabledState; +} FFWifiAndroidCodes; + +// Resolves both codes and reports the one outcome the module cannot work around: a jar that does not +// declare `getConnectionInfo` has no connection to report. The radio state is a refinement -- the +// module already carries on when the service will not answer it -- so it is allowed to come back as +// the sentinel, and what that costs is decided where it is used rather than here. +static const char* resolveCodes(FFWifiAndroidCodes* codes) { + const FFDexStaticIntRequest requests[] = { + { FF_WIFI_ANDROID_STUB, FF_WIFI_ANDROID_GET_CONNECTION_INFO, &codes->getConnectionInfo }, + { FF_WIFI_ANDROID_STUB, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, &codes->getWifiEnabledState }, + }; + + const char* error = ffDexStaticInts(FF_WIFI_ANDROID_JAR, requests, ARRAY_SIZE(requests)); if (error != nullptr) { + FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_WIFI_ANDROID_JAR, error); return error; } + if (codes->getConnectionInfo == FF_DEX_STATIC_INT_UNRESOLVED) { + return "The Wifi service does not declare getConnectionInfo"; + } + return nullptr; +} + +// Calls a method of the service that takes no arguments and answers with an int. `transaction` comes +// out of the jar, and the sentinel the reader leaves for a code it could not find is caught here: a +// release that does not declare the method is a message rather than a number that means something +// else. `transactionField` is carried only to name the method in the debug output, which a release +// build compiles out -- hence the attribute. +static const char* callIntMethod(FFBinder* binder, uint32_t handle, [[maybe_unused]] const char* transactionField, int32_t transaction, int32_t* result) { + if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) { + return "The Wifi service does not declare that method"; + } uint8_t parcelBuffer[256]; FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); @@ -843,7 +872,7 @@ static const char* callIntMethod(FFBinder* binder, uint32_t handle, const char* uint8_t replyBuffer[64]; FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); - error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); + const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply); if (error != nullptr) { return error; } @@ -880,10 +909,11 @@ static const char* detectWithBinder(FFlist* result) { 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); + // Resolved before the binder is opened, so that a jar which does not declare the method says so + // rather than the module failing later with a message about the transport. + FFWifiAndroidCodes codes = {}; + const char* error = resolveCodes(&codes); if (error != nullptr) { - FF_DEBUG("Reading the transaction code from \"%s\" failed: %s", FF_WIFI_ANDROID_JAR, error); return error; } @@ -901,7 +931,7 @@ static const char* detectWithBinder(FFlist* result) { return error; } FF_DEBUG("The \"%s\" service is handle %u, %s is transaction %d", - FF_WIFI_ANDROID_SERVICE, service.handle, FF_WIFI_ANDROID_GET_CONNECTION_INFO, transaction); + FF_WIFI_ANDROID_SERVICE, service.handle, FF_WIFI_ANDROID_GET_CONNECTION_INFO, codes.getConnectionInfo); uint8_t parcelBuffer[FF_WIFI_ANDROID_PARCEL_SIZE]; FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); @@ -912,7 +942,7 @@ static const char* detectWithBinder(FFlist* result) { uint8_t replyBuffer[FF_WIFI_ANDROID_REPLY_SIZE]; FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); - error = ffBinderTransact(&binder, service.handle, (uint32_t) transaction, 0, &parcel, &reply); + error = ffBinderTransact(&binder, service.handle, (uint32_t) codes.getConnectionInfo, 0, &parcel, &reply); if (error != nullptr) { return error; } @@ -939,7 +969,7 @@ static const char* detectWithBinder(FFlist* result) { // 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, service.handle, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, &state); + const char* stateError = callIntMethod(&binder, service.handle, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, codes.getWifiEnabledState, &state); if (stateError == nullptr && state != FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN) { connection.up = state != FF_WIFI_ANDROID_WIFI_STATE_DISABLED; connection.upKnown = true;