diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3786e6d..210534695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ Changes: Features: * Added Sound detection on Android. (Sound, Android) +* Added display detection on Android over binder, which needs neither a child process nor a permission. + * The module took 16.0-26.5 ms on the test device and now takes 1.3-1.5 ms. The `cmd` and `dumpsys` routes are unchanged and remain the fallback. + * It is also the only route that answers for an app UID on Android 12 and older, where `cmd display get-displays` does not exist and `dumpsys` is behind `android.permission.DUMP`. (Display, Android) * Added battery temperature, remaining time and charger type (AC / USB / wireless) detection on Android. (Battery, Android) * Performance improvements & internal cleanups * Added Finnish language support. (General) diff --git a/src/common/android/dex.c b/src/common/android/dex.c index 992217186..cd935e84e 100644 --- a/src/common/android/dex.c +++ b/src/common/android/dex.c @@ -20,7 +20,10 @@ #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_SIZE 0x50 #define FF_DEX_OFF_FIELD_IDS 0x54 +#define FF_DEX_OFF_METHOD_IDS_SIZE 0x58 +#define FF_DEX_OFF_METHOD_IDS 0x5C #define FF_DEX_OFF_CLASS_DEFS_SIZE 0x60 #define FF_DEX_OFF_CLASS_DEFS 0x64 @@ -32,8 +35,25 @@ // 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_TYPE 2 #define FF_DEX_OFF_FIELD_ID_NAME 4 +// method_id_item: [u16 class_idx][u16 proto_idx][u32 name_idx] +#define FF_DEX_METHOD_ID_SIZE 8 +#define FF_DEX_OFF_METHOD_ID_NAME 4 + +// code_item: [u16 registers_size][u16 ins_size][u16 outs_size][u16 tries_size][u32 debug_info_off] +// [u32 insns_size][u16 insns...]. Everything before the instruction stream is skipped by reading +// `insns_size` and starting `insns` where it ends. +#define FF_DEX_OFF_CODE_INSNS_SIZE 12 +#define FF_DEX_OFF_CODE_INSNS 16 + +// The seven `iget*` opcodes, which are how a method reads an instance field. `iput*` shares the same +// format and the same operand, and a `writeToParcel` does not store to a field, so one range covers +// both and the walk does not have to tell them apart. +#define FF_DEX_OP_FIELD_FIRST 0x52 +#define FF_DEX_OP_FIELD_LAST 0x5F + // The zip local file header, from APPNOTE.TXT 4.3.7. Only the two length fields are read from it: // the payload starts right behind them, and the local extra field is allowed to differ from the // central one, so the offset cannot be computed from the directory alone. @@ -231,11 +251,25 @@ static const char* dexString(const uint8_t* dex, const uint8_t* end, uint32_t in // 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. +// +// `fieldIds` and `methodIds` are the two tables a class's own lists are resolved through: a +// class_data_item names its members by index into them, and the `name_idx` of the entry is what says +// which member it is. Holding them here rather than re-reading the header offset per class also +// turns the per-member bounds check into a count comparison, which has to happen before the index is +// multiplied out anyway -- on a 32-bit build `index * 8` wraps for a large enough index and the +// range check that follows passes again. +// +// `string_ids` is deliberately not held here: `dexString` checks the one entry it reads, and a table +// nothing walks in order does not need a second check. typedef struct FFDexTables { const uint8_t* dex; const uint8_t* end; const uint8_t* types; uint32_t typeCount; + const uint8_t* fieldIds; + uint32_t fieldIdCount; + const uint8_t* methodIds; + uint32_t methodIdCount; const uint8_t* classes; uint32_t classCount; } FFDexTables; @@ -256,6 +290,18 @@ static const char* dexOpen(const uint8_t* dex, size_t size, FFDexTables* tables) return "The dex type table is out of range"; } + const uint8_t* fieldIds = dex + dexU32(dex + FF_DEX_OFF_FIELD_IDS); + const uint32_t fieldIdCount = dexU32(dex + FF_DEX_OFF_FIELD_IDS_SIZE); + if (!dexTableInRange(dex, end, fieldIds, fieldIdCount, FF_DEX_FIELD_ID_SIZE)) { + return "The dex field table is out of range"; + } + + const uint8_t* methodIds = dex + dexU32(dex + FF_DEX_OFF_METHOD_IDS); + const uint32_t methodIdCount = dexU32(dex + FF_DEX_OFF_METHOD_IDS_SIZE); + if (!dexTableInRange(dex, end, methodIds, methodIdCount, FF_DEX_METHOD_ID_SIZE)) { + return "The dex method table is out of range"; + } + 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 (!dexTableInRange(dex, end, classes, classCount, FF_DEX_CLASS_DEF_SIZE)) { @@ -266,11 +312,23 @@ static const char* dexOpen(const uint8_t* dex, size_t size, FFDexTables* tables) tables->end = end; tables->types = types; tables->typeCount = typeCount; + tables->fieldIds = fieldIds; + tables->fieldIdCount = fieldIdCount; + tables->methodIds = methodIds; + tables->methodIdCount = methodIdCount; tables->classes = classes; tables->classCount = classCount; return nullptr; } +// The descriptor a type index names, e.g. "I", "[F" or "Landroid/os/Parcel;". +static const char* dexTypeString(const FFDexTables* tables, uint32_t typeIndex) { + if (typeIndex >= tables->typeCount) { + return nullptr; + } + return dexString(tables->dex, tables->end, dexU32(tables->types + (size_t) typeIndex * 4)); +} + // 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 @@ -278,9 +336,10 @@ static const char* dexOpen(const uint8_t* dex, size_t size, FFDexTables* tables) // 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) { +// `typeIndexes` is parallel to `descriptors` 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 char* const* descriptors, 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; @@ -295,7 +354,7 @@ static void dexFindTypes(const FFDexTables* tables, const FFDexStaticIntRequest* continue; } for (uint32_t r = 0; r < count; ++r) { - if (settled[r] || typeIndexes[r] != UINT32_MAX || strcmp(descriptor, requests[r].classDescriptor) != 0) { + if (settled[r] || typeIndexes[r] != UINT32_MAX || strcmp(descriptor, descriptors[r]) != 0) { continue; } typeIndexes[r] = i; @@ -304,15 +363,113 @@ static void dexFindTypes(const FFDexTables* tables, const FFDexStaticIntRequest* } } +// The lists of a class_data_item, each positioned at its first entry. +// +// A class_data_item is `[uleb static_fields_size][uleb instance_fields_size][uleb direct_methods_size] +// [uleb virtual_methods_size]` followed by the four lists in that order, so the start of each list is +// only reachable by stepping over the one in front of it. That is done once here for the three +// readers that each want a different list: the static field reader pairs the first with the class's +// static value array, the write-sequence reader walks the methods, and the field-existence reader +// walks the second. +// +// A field entry is `[uleb field_idx_diff][uleb access_flags]` and a method entry is the same plus a +// trailing `[uleb code_off]`, which is the offset of the code_item holding its instructions -- or 0 +// for a method with no body, an abstract or native one. +// +// Each of the four lists is delta-encoded from zero on its own, so the counts are kept apart rather +// than summed: a class routinely has a direct method and a virtual one under the same index, and one +// accumulator carried across the two method lists reads the second list's names out of the wrong end +// of the method table. The field lists are read the same way, which is why the reader of each one +// starts its index at zero rather than continuing from the list in front of it. +typedef struct FFDexClassData { + const uint8_t* staticFields; + const uint8_t* instanceFields; + const uint8_t* methods; + uint32_t staticFieldCount; + uint32_t instanceFieldCount; + uint32_t directMethodCount; + uint32_t virtualMethodCount; +} FFDexClassData; + +// Steps over `count` field entries. A list that ends before its own count is a file that is not what +// it claims, not a short list: the count and the entries both come out of the same file. +static bool dexSkipFields(const uint8_t** p, const uint8_t* end, uint32_t count) { + for (uint32_t i = 0; i < count; ++i) { + if (*p >= end) { + return false; + } + (void) dexUleb128(p, end); // field_idx_diff + (void) dexUleb128(p, end); // access_flags + } + return true; +} + +static const char* dexReadClassData(const FFDexTables* tables, const uint8_t* classDef, FFDexClassData* data) { + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + const uint32_t classDataOff = dexU32(classDef + FF_DEX_OFF_CLASS_DEF_DATA); + if (classDataOff == 0) { + // A class with no class_data_item declares nothing of its own -- its members are all + // inherited. That is four empty lists, not a file that cannot be read, and the counts below + // are what carry the answer. + data->staticFields = nullptr; + data->instanceFields = nullptr; + data->methods = nullptr; + data->staticFieldCount = 0; + data->instanceFieldCount = 0; + data->directMethodCount = 0; + data->virtualMethodCount = 0; + return nullptr; + } + + const uint8_t* p = dex + classDataOff; + // One byte, so that the first uleb128 has something to read. + if (!dexInRange(dex, end, p, 1)) { + return "The dex class data is out of range"; + } + + data->staticFieldCount = dexUleb128(&p, end); + data->instanceFieldCount = dexUleb128(&p, end); + data->directMethodCount = dexUleb128(&p, end); + data->virtualMethodCount = dexUleb128(&p, end); + + data->staticFields = p; + if (!dexSkipFields(&p, end, data->staticFieldCount)) { + return "The dex class data is truncated"; + } + data->instanceFields = p; + if (!dexSkipFields(&p, end, data->instanceFieldCount)) { + return "The dex class data is truncated"; + } + data->methods = p; + return nullptr; +} + +// The class_def_item of the class that `typeIndex` names, or nullptr when this dex only references +// the class. +// +// Having a type index is not the same as being defined here: 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 in this +// one, with no class_def_item behind it. Only a class_def_item makes the answer this dex's, and +// without one the caller moves on to the next entry. +static const uint8_t* dexFindClassDef(const FFDexTables* tables, uint32_t typeIndex) { + 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) { + return classDef; + } + } + return nullptr; +} + // 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. +// `defined` says whether this dex *defines* the class, which the caller needs: a request the class +// cannot answer has to be left open when the class belongs to a later entry of the jar and closed +// when it does not. // // 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 @@ -320,91 +477,363 @@ static void dexFindTypes(const FFDexTables* tables, const FFDexStaticIntRequest* // 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* classDef = dexFindClassDef(tables, typeIndex); + *defined = classDef != nullptr; + if (classDef == nullptr) { + return nullptr; + } + 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) { + FFDexClassData classData; + const char* error = dexReadClassData(tables, classDef, &classData); + if (error != nullptr) { + return error; + } + + // The static value array is parallel to the static field list and covers exactly the same fields, + // which is what makes walking the two in step below meaningful. It is absent in two cases -- a + // class with no static field, and a class none of whose static fields has a constant value -- and + // the header writes 0 for it in both. The read is skipped rather than made against whatever byte + // a zero offset points at, which is the dex magic and decodes as a four byte int. + const uint8_t* fields = classData.staticFields; + const uint8_t* values = dex; + bool hasValues = false; + if (classData.staticFieldCount > 0) { + const uint32_t staticValuesOff = dexU32(classDef + FF_DEX_OFF_CLASS_DEF_STATIC_VALUES); + if (staticValuesOff != 0) { + values = dex + staticValuesOff; + if (!dexInRange(dex, end, values, 1)) { + return "The dex class data is out of range"; + } + if (classData.staticFieldCount != dexUleb128(&values, end)) { + return "The dex static fields and values do not pair up"; + } + hasValues = true; + } + } + + uint32_t fieldIndex = 0; + for (uint32_t j = 0; j < classData.staticFieldCount; ++j) { + // The two arrays are walked in step, so running out of either one is a truncation. + if (fields >= end || (hasValues && values >= end)) { + return "The dex static field list is truncated"; + } + + const uint32_t fieldIndexDiff = dexUleb128(&fields, end); // field_idx_diff + if (fieldIndexDiff > UINT32_MAX - fieldIndex) { + return "The dex field index is out of range"; + } + fieldIndex += fieldIndexDiff; + (void) dexUleb128(&fields, end); // access_flags + int32_t value = 0; + // Left false when there is no value array, which closes the field with the sentinel below: + // a class with no static value for the field has no answer for it. + bool isInt = false; + if (hasValues && !dexEncodedValue(&values, end, &value, &isInt)) { + return "The dex static value list is truncated"; + } + + if (fieldIndex >= tables->fieldIdCount) { + return "The dex field table is out of range"; + } + const char* name = dexString(dex, end, dexU32(tables->fieldIds + (size_t) fieldIndex * FF_DEX_FIELD_ID_SIZE + FF_DEX_OFF_FIELD_ID_NAME)); + if (name == nullptr) { 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); - // One byte each, so that the first uleb128 below has something to read. - if (!dexInRange(dex, end, fields, 1) || !dexInRange(dex, end, values, 1)) { - return "The dex class data is out of range"; + 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. + // 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; + } + *requests[r].result = value; + settled[r] = 1; } + } - const uint32_t staticFields = dexUleb128(&fields, end); - (void) dexUleb128(&fields, end); // instance_fields_size - (void) dexUleb128(&fields, end); // direct_methods_size - (void) dexUleb128(&fields, end); // virtual_methods_size - const uint32_t staticValues = dexUleb128(&values, end); - if (staticFields != staticValues) { - return "The dex static fields and values do not pair up"; + // 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; +} - const uint8_t* fieldIds = dex + dexU32(dex + FF_DEX_OFF_FIELD_IDS); - uint32_t fieldIndex = 0; - for (uint32_t j = 0; j < staticFields; ++j) { - // The two arrays are walked in step, so running out of either one is a truncation. - if (fields >= end || values >= end) { - return "The dex static field list is truncated"; - } +// --------------------------------------------------------------------------------------------- +// Walking a method's instructions +// --------------------------------------------------------------------------------------------- - const uint32_t fieldIndexDiff = dexUleb128(&fields, end); // field_idx_diff - if (fieldIndexDiff > UINT32_MAX - fieldIndex) { - return "The dex field index is out of range"; - } - fieldIndex += fieldIndexDiff; - (void) dexUleb128(&fields, end); // access_flags - int32_t value; - bool isInt; - if (!dexEncodedValue(&values, end, &value, &isInt)) { - return "The dex static value list is truncated"; - } +// The width of every instruction, in code units. This is ART's `Instruction::SizeInCodeUnits`, and +// the walk below cannot stay on the instruction grid without it -- a walk that leaves the grid still +// produces a plausible-looking list of fields, which is what makes this worth a table rather than a +// guess. Unused and odex-only opcodes are 1 rather than 0, so that a byte which is not an instruction +// at all moves the walk on instead of leaving it where it is. +static const uint8_t dexOpcodeSize[256] = { + /* 00 */ 1, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 1, 1, 1, 1, 1, + /* 10 */ 1, 1, 1, 2, 3, 2, 2, 3, 5, 2, 2, 3, 2, 1, 1, 2, + /* 20 */ 2, 1, 2, 2, 3, 3, 3, 1, 1, 2, 3, 3, 3, 2, 2, 2, + /* 30 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, + /* 40 */ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* 50 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* 60 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, + /* 70 */ 3, 3, 3, 1, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, + /* 80 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* 90 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* a0 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* b0 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* c0 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* d0 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + /* e0 */ 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* f0 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 3, 3, 2, 2, +}; - if (!dexTableInRange(dex, end, fieldIds, (uint64_t) fieldIndex + 1, FF_DEX_FIELD_ID_SIZE)) { +// The code_item at `codeOff`, with its instruction array range checked. nullptr is one that cannot +// be read: an offset outside the mapping, or an instruction count that runs past the end. +static const uint8_t* dexReadCode(const FFDexTables* tables, uint32_t codeOff) { + const uint8_t* code = tables->dex + codeOff; + if (!dexInRange(tables->dex, tables->end, code, FF_DEX_OFF_CODE_INSNS)) { + return nullptr; + } + const uint32_t insnsSize = dexU32(code + FF_DEX_OFF_CODE_INSNS_SIZE); + if (!dexTableInRange(tables->dex, tables->end, code + FF_DEX_OFF_CODE_INSNS, insnsSize, 2)) { + return nullptr; + } + return code; +} + +// Records `name` and `type` as the next field of a write sequence, unless the name is already in it. +// +// An array field is read twice: once for its length, which is the count written ahead of the elements, +// and again inside the loop that writes them. A class cannot declare two instance fields under one +// name, so a repeat is always that reload, and the first read is where the field belongs in the +// sequence. +// +// A name or descriptor longer than the caller's buffer is refused rather than truncated: half a name +// matches nothing, which is indistinguishable from the field not being in the class at all. Refusing +// one field leaves the whole sequence unwritten, because a caller walking a sequence with a hole in +// it would read every field behind the hole from the wrong place. +static bool dexParcelFieldAdd(FFDexParcelField* fields, uint32_t* count, uint32_t capacity, const char* name, const char* type) { + for (uint32_t i = 0; i < *count; ++i) { + if (strcmp(fields[i].name, name) == 0) { + return true; + } + } + if (*count >= capacity) { + return false; + } + const size_t nameSize = strlen(name) + 1; + const size_t typeSize = strlen(type) + 1; + if (nameSize > FF_DEX_PARCEL_FIELD_NAME_MAX || typeSize > FF_DEX_PARCEL_FIELD_TYPE_MAX) { + return false; + } + memcpy(fields[*count].name, name, nameSize); + memcpy(fields[*count].type, type, typeSize); + ++*count; + return true; +} + +// Walks the instruction grid of a code_item and records every instance field the method reads, in the +// order it reads them -- which is the order `writeToParcel` puts them on the wire. +// +// The grid is followed by opcode width, so the walk only ever lands on an instruction start, and it +// stops at the first payload pseudo-instruction. A `switch` or a `fill-array-data` leaves one behind +// every reachable instruction; its contents are data rather than instructions, and stepping into it +// desynchronises the grid for good. +// +// `iget*` and `iput*` share a format and put the field index in the code unit behind the opcode in +// both, which is what lets one test cover the whole range. A `writeToParcel` does not store to a +// field, so the `iput` half is never taken -- it is in the range because splitting it would be a +// claim about the method that the opcode does not make. +static const char* dexWalkParcelFields(const FFDexTables* tables, const uint8_t* code, FFDexParcelField* fields, uint32_t capacity, uint32_t* count) { + *count = 0; + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + const uint32_t insnsSize = dexU32(code + FF_DEX_OFF_CODE_INSNS_SIZE); + const uint8_t* insns = code + FF_DEX_OFF_CODE_INSNS; + + for (uint32_t i = 0; i < insnsSize;) { + const uint16_t unit = dexU16(insns + (size_t) i * 2); + // The three payload pseudo-instructions put their ident in the high byte and zero in the low + // one. `nop` is the one instruction whose low byte is zero as well, and it is told apart by + // the whole code unit being zero. Testing the high byte alone is what gets this wrong: a + // `sparse-switch` over register 2 encodes as 0x022c, which is an instruction, not a payload. + if ((unit & 0xFF) == 0 && unit != 0) { + break; + } + const uint32_t opcode = unit & 0xFF; + if (opcode >= FF_DEX_OP_FIELD_FIRST && opcode <= FF_DEX_OP_FIELD_LAST) { + if (i + 1 >= insnsSize) { + break; + } + const uint32_t fieldIndex = dexU16(insns + ((size_t) i + 1) * 2); + if (fieldIndex >= tables->fieldIdCount) { 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)); + const uint8_t* fieldId = tables->fieldIds + (size_t) fieldIndex * FF_DEX_FIELD_ID_SIZE; + const char* name = dexString(dex, end, dexU32(fieldId + FF_DEX_OFF_FIELD_ID_NAME)); + const char* type = dexTypeString(tables, dexU16(fieldId + FF_DEX_OFF_FIELD_ID_TYPE)); + if (name == nullptr || type == nullptr) { + return "The dex field is not resolvable"; + } + if (!dexParcelFieldAdd(fields, count, capacity, name, type)) { + FF_DEBUG("\"%s\" (%s) does not fit a sequence of %u fields", name, type, capacity); + *count = 0; + return nullptr; + } + } + i += dexOpcodeSize[opcode]; + } + return nullptr; +} + +// Walks the method list of the class that `typeIndex` names, and for every request whose method name +// it holds walks that method's instructions. `defined` is what `dexClassStaticInts` reports, for the +// same reason. +// +// A request the class does not answer is closed with a count of 0 -- the count a caller reads as +// "this build does not write that class", which is the truth: the class is this dex's, so no later +// entry of the jar has the method either. +static const char* dexClassParcelFields(const FFDexTables* tables, uint32_t typeIndex, const FFDexParcelRequest* requests, const uint32_t* typeIndexes, uint8_t* settled, uint32_t count, bool* defined) { + const uint8_t* classDef = dexFindClassDef(tables, typeIndex); + *defined = classDef != nullptr; + if (classDef == nullptr) { + return nullptr; + } + + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + + FFDexClassData classData; + const char* error = dexReadClassData(tables, classDef, &classData); + if (error != nullptr) { + return error; + } + + const uint8_t* methods = classData.methods; + // Two runs, each delta-encoded from zero on its own -- so the index is reset per run rather than + // carried across, and a direct method and a virtual one under the same index both resolve. + for (uint32_t list = 0; list < 2; ++list) { + const uint32_t listCount = list == 0 ? classData.directMethodCount : classData.virtualMethodCount; + uint32_t methodIndex = 0; + for (uint32_t j = 0; j < listCount; ++j) { + if (methods >= end) { + return "The dex class data is truncated"; + } + const uint32_t methodIndexDiff = dexUleb128(&methods, end); // method_idx_diff + if (methodIndexDiff > UINT32_MAX - methodIndex) { + return "The dex method index is out of range"; + } + methodIndex += methodIndexDiff; + (void) dexUleb128(&methods, end); // access_flags + const uint32_t codeOff = dexUleb128(&methods, end); + + if (methodIndex >= tables->methodIdCount) { + return "The dex method table is out of range"; + } + const char* name = dexString(dex, end, dexU32(tables->methodIds + (size_t) methodIndex * FF_DEX_METHOD_ID_SIZE + FF_DEX_OFF_METHOD_ID_NAME)); 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. 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; - } - *requests[r].result = value; - settled[r] = 1; - } - } - // 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; + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] != typeIndex || strcmp(name, requests[r].methodName) != 0) { + continue; + } + // A method with no body -- abstract, or native -- writes nothing, and the count stays 0. + const uint8_t* code = codeOff == 0 ? nullptr : dexReadCode(tables, codeOff); + if (codeOff != 0 && code == nullptr) { + return "The dex code is out of range"; + } + if (code != nullptr) { + error = dexWalkParcelFields(tables, code, requests[r].fields, requests[r].capacity, requests[r].count); + if (error != nullptr) { + return error; + } + } settled[r] = 1; } } + } + + // The class is defined here, so a request of its own that went unanswered is one whose method the + // class does not declare. The count it already holds is the 0 that says so. + for (uint32_t r = 0; r < count; ++r) { + if (!settled[r] && typeIndexes[r] == typeIndex) { + settled[r] = 1; + } + } + return nullptr; +} + +// Walks the instance field list of the class that `typeIndex` names and answers every request whose +// field name it holds. `defined` as above. +// +// This is the question a Parcelable whose layout is not field-driven needs: `DisplayCutout` reads +// everything through compiler-synthesised accessors, so the set of fields its class declares is the +// only thing that says how long its body is. +static const char* dexClassInstanceFields(const FFDexTables* tables, uint32_t typeIndex, const FFDexInstanceFieldRequest* requests, const uint32_t* typeIndexes, uint8_t* settled, uint32_t count, bool* defined) { + const uint8_t* classDef = dexFindClassDef(tables, typeIndex); + *defined = classDef != nullptr; + if (classDef == nullptr) { return nullptr; } - // 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. + + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + + FFDexClassData classData; + const char* error = dexReadClassData(tables, classDef, &classData); + if (error != nullptr) { + return error; + } + + const uint8_t* fields = classData.instanceFields; + uint32_t fieldIndex = 0; + for (uint32_t j = 0; j < classData.instanceFieldCount; ++j) { + if (fields >= end) { + return "The dex class data is truncated"; + } + const uint32_t fieldIndexDiff = dexUleb128(&fields, end); // field_idx_diff + if (fieldIndexDiff > UINT32_MAX - fieldIndex) { + return "The dex field index is out of range"; + } + fieldIndex += fieldIndexDiff; + (void) dexUleb128(&fields, end); // access_flags + + if (fieldIndex >= tables->fieldIdCount) { + return "The dex field table is out of range"; + } + const char* name = dexString(dex, end, dexU32(tables->fieldIds + (size_t) fieldIndex * FF_DEX_FIELD_ID_SIZE + FF_DEX_OFF_FIELD_ID_NAME)); + 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; + } + *requests[r].result = true; + settled[r] = 1; + } + } + + // A field the class does not declare is a `false`, which the results already hold. + for (uint32_t r = 0; r < count; ++r) { + if (!settled[r] && typeIndexes[r] == typeIndex) { + settled[r] = 1; + } + } return nullptr; } @@ -413,10 +842,8 @@ static const char* dexClassStaticInts(const FFDexTables* tables, uint32_t typeIn // --------------------------------------------------------------------------------------------- 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` - size_t size; } FFDexMapping; static void wrapDexMapping(FFDexMapping* mapping) { @@ -426,6 +853,33 @@ static void wrapDexMapping(FFDexMapping* mapping) { } } +// Maps the jar read-only. The pages the walk touches are the only ones faulted in, which is what +// keeps a 52 MB framework.jar off the heap and out of the cost of a lookup. The mapping outlives the +// descriptor, so the descriptor is closed before this returns. +static const char* dexMapJar(const char* jarPath, FFDexMapping* mapping) { + FF_AUTO_CLOSE_FD int fd = open(jarPath, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + FF_DEBUG("open(%s) failed: %s", jarPath, strerror(errno)); + return "open(jar) failed"; + } + + struct stat st = {}; + const int statStatus = fstat(fd, &st); + if (statStatus != 0 || st.st_size <= 0) { + FF_DEBUG("fstat(%s) reported %s (%lld bytes)", jarPath, + statStatus != 0 ? strerror(errno) : "an empty file", (long long) st.st_size); + return "fstat(jar) failed"; + } + mapping->mappedSize = (size_t) st.st_size; + mapping->mapped = mmap(nullptr, mapping->mappedSize, PROT_READ, MAP_PRIVATE, fd, 0); + if (mapping->mapped == MAP_FAILED) { + FF_DEBUG("mmap(%s, %zu bytes) failed: %s", jarPath, mapping->mappedSize, strerror(errno)); + mapping->mapped = nullptr; + return "mmap(jar) failed"; + } + return nullptr; +} + // Locates an entry through the jar's central directory. // // The sizes and the compression method are read from the directory rather than from the entry's own @@ -584,6 +1038,74 @@ static const char* inflateDex(const uint8_t* data, size_t dataSize, uint32_t unc } #endif +// The dex bytes of one jar entry, and the validated tables of the dex they hold. +typedef struct FFDexEntry { + const uint8_t* data; + size_t size; + FFDexTables tables; +} FFDexEntry; + +// Locates `classesN.dex`, inflates it when the entry is deflated, and validates the header. +// +// `present` reports whether the jar holds the entry at all. A jar spreads its classes over as many +// entries as it needs and then simply runs out, so an entry that is not there ends the walk rather +// than failing it -- except for the first, which every jar has and which is where the magic scan +// stands in for a directory that cannot be read. `inflated` receives the buffer the caller has to +// release when the entry needed one, and is left alone otherwise. +static const char* dexOpenEntry(const FFDexMapping* mapping, uint32_t index, bool* present, uint8_t** inflated, FFDexEntry* entry) { + // Wide enough for `classes999.dex`, which is the longest name `FF_DEX_MAX_ENTRIES` can ask for. + char name[sizeof("classes999.dex")]; + if (index == 1) { + memcpy(name, FF_DEX_ENTRY, sizeof(FF_DEX_ENTRY)); + } else { + (void) snprintf(name, sizeof(name), "classes%u.dex", index); + } + *present = true; + + 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, name, &data, &dataSize, &uncompressedSize, &method) != nullptr) { + if (index > 1) { + // The magic scan only ever finds the first dex, so it cannot stand in for a later entry: + // running past the end of the entries that exist ends the walk. + *present = false; + return nullptr; + } + data = findDexMagic(mapping->mapped, mapping->mappedSize, &dataSize); + if (data == nullptr) { + FF_DEBUG("Neither %s nor a dex magic signature is in the %zu byte jar", name, mapping->mappedSize); + return "No dex in the jar"; + } + } else if (method == FF_ZIP_METHOD_DEFLATED) { + #ifdef FF_HAVE_ZLIB + const char* error = inflateDex(data, dataSize, uncompressedSize, inflated); + if (error != nullptr) { + return error; + } + data = *inflated; + dataSize = uncompressedSize; + #else + (void) inflated; // the parameter is only written on the inflating path + return "The jar deflates its dex entries and fastfetch was built without zlib"; + #endif + } else if (method != FF_ZIP_METHOD_STORED) { + FF_DEBUG("%s uses compression method %u, only %u (stored) and %u (deflated) are handled", + name, method, FF_ZIP_METHOD_STORED, FF_ZIP_METHOD_DEFLATED); + return "A dex entry uses an unsupported compression method"; + } + // A STORED entry needs no further work: `dataSize` already holds the `compressedSize` that + // `findDexEntry` checked against the mapping, and for a STORED entry the payload is the file + // itself. The header's `uncompressedSize` is deliberately not used for it -- nothing bounds that + // value, so a corrupt one reaches past the end of the mapping, which is exactly what the dex + // header would then be validated against. + + entry->data = data; + entry->size = dataSize; + return dexOpen(entry->data, entry->size, &entry->tables); +} + 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 @@ -596,41 +1118,25 @@ const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* re } [[gnu::cleanup(wrapDexMapping)]] FFDexMapping mapping = {}; - - { - FF_AUTO_CLOSE_FD int fd = open(jarPath, O_RDONLY | O_CLOEXEC); - if (fd < 0) { - FF_DEBUG("open(%s) failed: %s", jarPath, strerror(errno)); - return "open(jar) failed"; - } - - struct stat st = {}; - const int statStatus = fstat(fd, &st); - if (statStatus != 0 || st.st_size <= 0) { - FF_DEBUG("fstat(%s) reported %s (%lld bytes)", jarPath, - statStatus != 0 ? strerror(errno) : "an empty file", (long long) st.st_size); - 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. - if (mapping.mapped == MAP_FAILED) { - FF_DEBUG("mmap(%s, %zu bytes) failed: %s", jarPath, mapping.mappedSize, strerror(errno)); - mapping.mapped = nullptr; - return "mmap(jar) failed"; - } + const char* error = dexMapJar(jarPath, &mapping); + if (error != nullptr) { + return error; } - // 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. + // One byte per request saying whether it is closed -- answered, or given up on -- the type index + // it was found under in the entry being walked, and the descriptor that index was found for. All + // three are sized from `count` rather than from a fixed bound, and all three 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) { - FF_DEBUG("Allocating %zu bytes for %u static int requests failed", - (size_t) count * (1 + sizeof(uint32_t)), count); + FF_AUTO_FREE const char** descriptors = malloc((size_t) count * sizeof(const char*)); + if (settled == nullptr || typeIndexes == nullptr || descriptors == nullptr) { + FF_DEBUG("Allocating the tables for %u static int requests failed", count); return "malloc failed"; } + for (uint32_t r = 0; r < count; ++r) { + descriptors[r] = requests[r].classDescriptor; + } // 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 @@ -643,68 +1149,23 @@ const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* re // 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)); - } else { - (void) snprintf(entry, sizeof(entry), "classes%u.dex", index); - } - - const uint8_t* data = nullptr; - 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) { - // The magic scan only ever finds the first dex, so it cannot stand in for a later - // entry: running past the end of the entries that exist ends the walk. - break; - } - data = findDexMagic(mapping.mapped, mapping.mappedSize, &dataSize); - if (data == nullptr) { - FF_DEBUG("Neither %s nor a dex magic signature is in the %zu byte jar", entry, mapping.mappedSize); - return "No dex in the jar"; - } - } else if (method == FF_ZIP_METHOD_DEFLATED) { - #ifdef FF_HAVE_ZLIB - error = inflateDex(data, dataSize, uncompressedSize, &inflated); - if (error != nullptr) { - return error; - } - data = inflated; - dataSize = uncompressedSize; - #else - return "The jar deflates its dex entries and fastfetch was built without zlib"; - #endif - } else if (method != FF_ZIP_METHOD_STORED) { - FF_DEBUG("%s uses compression method %u, only %u (stored) and %u (deflated) are handled", - entry, method, FF_ZIP_METHOD_STORED, FF_ZIP_METHOD_DEFLATED); - return "A dex entry uses an unsupported compression method"; - } - // A STORED entry needs no further work: `dataSize` already holds the `compressedSize` that - // `findDexEntry` checked against the mapping, and for a STORED entry the payload is the file - // itself. The header's `uncompressedSize` is deliberately not used for it -- nothing bounds - // that value, so a corrupt one reaches past the end of the mapping, which is exactly what the - // dex header would then be validated against. - - mapping.data = data; - mapping.size = dataSize; - - FFDexTables tables = {}; - error = dexOpen(mapping.data, mapping.size, &tables); + FFDexEntry entry = {}; + bool present = false; + error = dexOpenEntry(&mapping, index, &present, &inflated, &entry); if (error != nullptr) { // The dex cannot be read at all, which no later entry can fix. return error; } + if (!present) { + break; + } - dexFindTypes(&tables, requests, settled, typeIndexes, count); + dexFindTypes(&entry.tables, descriptors, 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 @@ -716,7 +1177,7 @@ const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* re } const uint32_t typeIndex = typeIndexes[r]; bool defined = false; - error = dexClassStaticInts(&tables, typeIndex, requests, typeIndexes, settled, count, &defined); + error = dexClassStaticInts(&entry.tables, typeIndex, requests, typeIndexes, settled, count, &defined); if (error != nullptr) { // The dex cannot be read, which no later entry can fix. return error; @@ -752,3 +1213,163 @@ const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* re } return nullptr; } + +const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* requests, uint32_t count) { + // Every request is written a count of 0 before anything can fail, so that a caller reads an + // answer rather than whatever its own stack held there, however this returns. + for (uint32_t r = 0; r < count; ++r) { + *requests[r].count = 0; + } + if (count == 0) { + return nullptr; + } + + [[gnu::cleanup(wrapDexMapping)]] FFDexMapping mapping = {}; + const char* error = dexMapJar(jarPath, &mapping); + if (error != nullptr) { + return error; + } + + FF_AUTO_FREE uint8_t* settled = calloc(count, 1); + FF_AUTO_FREE uint32_t* typeIndexes = malloc((size_t) count * sizeof(uint32_t)); + FF_AUTO_FREE const char** descriptors = malloc((size_t) count * sizeof(const char*)); + if (settled == nullptr || typeIndexes == nullptr || descriptors == nullptr) { + FF_DEBUG("Allocating the tables for %u parcel field requests failed", count); + return "malloc failed"; + } + for (uint32_t r = 0; r < count; ++r) { + descriptors[r] = requests[r].classDescriptor; + } + + uint32_t pending = count; + for (uint32_t index = 1; index <= FF_DEX_MAX_ENTRIES && pending > 0; ++index) { + FF_AUTO_FREE uint8_t* inflated = nullptr; + FFDexEntry entry = {}; + bool present = false; + error = dexOpenEntry(&mapping, index, &present, &inflated, &entry); + if (error != nullptr) { + // The dex cannot be read at all, which no later entry can fix. + return error; + } + if (!present) { + break; + } + + dexFindTypes(&entry.tables, descriptors, settled, typeIndexes, count); + + 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 = dexClassParcelFields(&entry.tables, typeIndex, requests, typeIndexes, settled, count, &defined); + if (error != nullptr) { + return error; + } + if (!defined) { + 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; + } + } + } + + // A request still holding 0 is one whose class no entry of the jar defines, or whose class does + // not declare the method. Both are the same answer -- this build does not write that class -- and + // for a debug build it is worth saying which of the two it was, since the count 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].count == 0) { + FF_DEBUG("The write sequence of \"%s.%s\" is not in the jar", requests[r].classDescriptor, requests[r].methodName); + } + } + return nullptr; +} + +const char* ffDexInstanceFields(const char* jarPath, const FFDexInstanceFieldRequest* requests, uint32_t count) { + // Every request is written `false` before anything can fail, so that a caller reads an answer + // rather than whatever its own stack held there, however this returns. + for (uint32_t r = 0; r < count; ++r) { + *requests[r].result = false; + } + if (count == 0) { + return nullptr; + } + + [[gnu::cleanup(wrapDexMapping)]] FFDexMapping mapping = {}; + const char* error = dexMapJar(jarPath, &mapping); + if (error != nullptr) { + return error; + } + + FF_AUTO_FREE uint8_t* settled = calloc(count, 1); + FF_AUTO_FREE uint32_t* typeIndexes = malloc((size_t) count * sizeof(uint32_t)); + FF_AUTO_FREE const char** descriptors = malloc((size_t) count * sizeof(const char*)); + if (settled == nullptr || typeIndexes == nullptr || descriptors == nullptr) { + FF_DEBUG("Allocating the tables for %u instance field requests failed", count); + return "malloc failed"; + } + for (uint32_t r = 0; r < count; ++r) { + descriptors[r] = requests[r].classDescriptor; + } + + uint32_t pending = count; + for (uint32_t index = 1; index <= FF_DEX_MAX_ENTRIES && pending > 0; ++index) { + FF_AUTO_FREE uint8_t* inflated = nullptr; + FFDexEntry entry = {}; + bool present = false; + error = dexOpenEntry(&mapping, index, &present, &inflated, &entry); + if (error != nullptr) { + // The dex cannot be read at all, which no later entry can fix. + return error; + } + if (!present) { + break; + } + + dexFindTypes(&entry.tables, descriptors, settled, typeIndexes, count); + + 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 = dexClassInstanceFields(&entry.tables, typeIndex, requests, typeIndexes, settled, count, &defined); + if (error != nullptr) { + return error; + } + if (!defined) { + 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; + } + } + } + + for (uint32_t r = 0; r < count; ++r) { + if (!*requests[r].result) { + FF_DEBUG("\"%s\" is not an instance 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 6c324d8c3..75ca4acb4 100644 --- a/src/common/android/dex.h +++ b/src/common/android/dex.h @@ -62,3 +62,102 @@ typedef struct FFDexStaticIntRequest { // 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); + +// --------------------------------------------------------------------------------------------- +// Reading a Parcelable's wire layout +// --------------------------------------------------------------------------------------------- +// +// A Parcelable's `writeToParcel` puts its fields on the wire in the order the method reads them, and +// that order is the only thing that says where each one is. It is not in the class's declaration: a +// dex stores a class's instance fields sorted by field index, which is sorted by *name*, so the +// declaration order a reader would want is not recorded anywhere but the method body. +// +// Recovering it therefore means walking the bytecode. The compiler emits +// +// iget v0, p0, LDisplayInfo;->logicalWidth:I +// invoke-virtual {p1, v0}, Landroid/os/Parcel;->writeInt(I)V +// +// once per field, in write order, so the `iget` opcodes in a linear pass over the instruction grid +// are the sequence. Two things have to be got right for that pass to stay on the grid, and both are +// easy to get wrong in a way that still produces a plausible-looking list: +// +// * Instructions are 1, 2, 3, 4 or 5 code units wide, and the width is a property of the opcode. +// The table below is ART's `Instruction::SizeInCodeUnits`. +// * The payload pseudo-instructions a `switch` or a `fill-array-data` leaves in the stream have a +// low byte of zero and a high byte of 1, 2 or 3. They are not instructions, they sit behind every +// reachable instruction, and stepping into one desynchronises the grid -- so the walk stops at the +// first of them rather than trying to step over it. +// +// An array field is read twice: once for its `length`, which is the count that goes on the wire, and +// again inside the loop that writes the elements. A class cannot declare two instance fields under +// one name, so a repeat is always that reload, and the first occurrence is where the field is +// written. The reader de-duplicates by name for that reason. +// +// What is *not* recovered is how each field is framed -- `writeString` against `writeString8`, +// `writeParcelable` against `writeTypedObject`. That is a property of the method the field is handed +// to, not of the field, and two `String` fields of the same class can differ: `DisplayInfo.name` goes +// through `writeString8` while `DeviceProductInfo.mName` goes through `writeString`. Telling them +// apart would mean resolving every invoke target, so the type descriptor is reported instead and the +// caller frames the fields its descriptor settles -- an `I`, a `J`, an `[F` -- and names the few it +// does not. Measured over three releases' own `framework.jar`, that is four names per class. +// +// The point of asking the device rather than carrying a table is that the field set moves: `DisplayInfo` +// has 34 instance fields on Android 11, 42 on 13 and 59 on 16, and the fields that arrived in between +// (`displayGroupId`, `renderFrameRate`, `appsSupportedModes`, `committedState`, ...) are exactly the +// ones a parser written against one release reads out of the middle of another. Reading the sequence +// from the build that is running makes every one of them optional by construction. + +// A field name and a type descriptor are ASCII and short -- the longest name in `DisplayInfo` is 28 +// bytes and the longest descriptor `Landroid/hardware/display/DeviceProductInfo$ManufactureDate;` is +// 60. A longer one is a class this reader was not written for, and is reported as unresolved rather +// than truncated: half a name would silently match nothing, which is indistinguishable from the field +// not being there. +#define FF_DEX_PARCEL_FIELD_NAME_MAX 40 +#define FF_DEX_PARCEL_FIELD_TYPE_MAX 72 + +// One field of the write sequence. `type` is the dex type descriptor, e.g. "I", "[F", +// "Ljava/lang/String;" or "Landroid/view/DisplayAddress;". +typedef struct FFDexParcelField { + char name[FF_DEX_PARCEL_FIELD_NAME_MAX]; + char type[FF_DEX_PARCEL_FIELD_TYPE_MAX]; +} FFDexParcelField; + +// One `.` write sequence, and where it is written. +typedef struct FFDexParcelRequest { + const char* classDescriptor; + const char* methodName; + FFDexParcelField* fields; + uint32_t capacity; + uint32_t* count; // how many fields the sequence holds, or 0 when the jar cannot answer +} FFDexParcelRequest; + +// Resolves the write sequence of every request out of the dex entries of `jarPath`, walking +// `classes.dex`, `classes2.dex`, ... in order until one of them defines the class -- the same walk, +// and the same one pass over the type table, that ffDexStaticInts makes. +// +// Every request is written a count or 0. A class the jar does not declare, a method it does not +// declare, and a sequence longer than the caller's `capacity` all leave 0 behind, and none of them +// holds back the others: they are independent questions about the same pass. A count of 0 is not a +// failure but an answer -- this build does not write that class, so a caller reading it has nothing +// to walk. The return value is reserved for the jar itself, as in ffDexStaticInts. +[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* requests, uint32_t count); + +// Whether `` declares an instance field named ``. +// +// This is the question a Parcelable whose layout is not field-driven needs. `DisplayCutout` is one: +// its `writeCutoutToParcel` reads everything through `-$$Nest$` accessors the compiler synthesised, +// so there is no `iget` to walk and no class name on the wire either. What does say how long its body +// is, is which fields the class carries -- Android 11 declares three, Android 13 adds +// `mCutoutPathParserInfo` and Android 16 adds `mSideOverrides`, and each one adds a known run of +// writes behind the three both have. The declaration is a field set rather than an order, which is +// exactly what this answers. +typedef struct FFDexInstanceFieldRequest { + const char* classDescriptor; + const char* fieldName; + bool* result; +} FFDexInstanceFieldRequest; + +// Writes `false` for every request the jar cannot answer, on every path out -- a class the jar does +// not declare and a field the class does not declare are the same answer here, because both mean the +// field is not in the parcel. The return value is reserved for the jar itself. +[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexInstanceFields(const char* jarPath, const FFDexInstanceFieldRequest* requests, uint32_t count); diff --git a/src/detection/displayserver/displayserver_android.c b/src/detection/displayserver/displayserver_android.c index 14114c0c6..616e2ab56 100644 --- a/src/detection/displayserver/displayserver_android.c +++ b/src/detection/displayserver/displayserver_android.c @@ -1,6 +1,9 @@ #include "displayserver.h" #include "common/android/api.h" +#include "common/android/binder.h" +#include "common/android/dex.h" #include "common/arrutil.h" +#include "common/debug.h" #include "common/settings.h" #include "common/strutil.h" #include "common/processing.h" @@ -331,6 +334,1196 @@ static bool detectWithDumpsys(FFDisplayServerResult* ds) { "dumpsys"); } +// --------------------------------------------------------------------------------------------- +// The binder route +// --------------------------------------------------------------------------------------------- +// +// `cmd display get-displays` and `dumpsys display` are both a child process, and that fork is all of +// what this module costs: it took 16.0-26.5 ms on the test device, where `cmd display get-displays` +// itself takes 9.2 ms and a bare fork 8.6 ms. `cmd` does not exist before Android 13 either, so on +// Android 11 and 12 an app UID has no display at all -- `dumpsys` is behind +// android.permission.DUMP there. `display` is the service both sit on, +// android.hardware.display.IDisplayManager: it lives in system_server, an app UID may call it, and +// one `getDisplayInfo(0)` answers the whole `DisplayInfo` the dump prints, field for field. With the +// fork gone the module takes 1.3-1.5 ms. +// +// What it costs is the parse. The reply is a Java Parcelable and its field set moves with every +// release and every vendor fork -- `DisplayInfo` writes 34 fields on Android 11, 42 on 13 and 57 on +// 16 -- so neither a fixed offset nor a sequence written against one release survives. The sequence +// is read out of the device's own `framework.jar` instead, from the `iget` order of +// `DisplayInfo.writeToParcel`, and the walk below follows it: a field the release does not write is +// simply not in the sequence, which is what makes every one of them optional by construction. See +// common/android/dex.h for how the sequence is recovered and what it does not say. +// +// What the sequence does not say is how each field is framed: `writeString` and `writeString8` are +// both a `Ljava/lang/String;` on the wire and are not the same length. The type descriptor settles +// the primitives and both array shapes, a handful of names settle the rest, and every nested +// Parcelable is anchored on its class name. A layout this does not recognise fails the route rather +// than answering, which is what makes the `cmd` / `dumpsys` fallback behind it worth having. + +#define FF_DISPLAY_ANDROID_SERVICE "display" +#define FF_DISPLAY_ANDROID_DESCRIPTOR "android.hardware.display.IDisplayManager" + +// The jar the reply's classes live in, and their dex type descriptors. All of them are asked for in +// one lookup: resolving a descriptor is dominated by a pass over the dex type table, and one pass +// answers every request whose class is named in it -- see common/android/dex.h. +#define FF_DISPLAY_ANDROID_JAR "/system/framework/framework.jar" + +#define FF_DISPLAY_ANDROID_CLASS_INFO "Landroid/view/DisplayInfo;" +#define FF_DISPLAY_ANDROID_CLASS_ADDRESS "Landroid/view/DisplayAddress$Physical;" +#define FF_DISPLAY_ANDROID_CLASS_PRODUCT_INFO "Landroid/hardware/display/DeviceProductInfo;" +#define FF_DISPLAY_ANDROID_CLASS_MANUFACTURE_DATE "Landroid/hardware/display/DeviceProductInfo$ManufactureDate;" +#define FF_DISPLAY_ANDROID_CLASS_HDR_CAPABILITIES "Landroid/view/Display$HdrCapabilities;" +#define FF_DISPLAY_ANDROID_CLASS_MODE "Landroid/view/Display$Mode;" +#define FF_DISPLAY_ANDROID_CLASS_FRAME_RATE_CATEGORY_RATE "Landroid/view/FrameRateCategoryRate;" +#define FF_DISPLAY_ANDROID_CLASS_CUTOUT "Landroid/view/DisplayCutout;" + +// The Java class names `writeParcelable` writes in front of each nested body, which is the anchor the +// walk hangs on. It writes the *runtime* class, so the abstract `DisplayAddress` a `DisplayInfo` +// declares arrives as its `$Physical` subclass. +#define FF_DISPLAY_ANDROID_NAME_ADDRESS "android.view.DisplayAddress$Physical" +#define FF_DISPLAY_ANDROID_NAME_PRODUCT_INFO "android.hardware.display.DeviceProductInfo" +#define FF_DISPLAY_ANDROID_NAME_MANUFACTURE_DATE "android.hardware.display.DeviceProductInfo$ManufactureDate" +#define FF_DISPLAY_ANDROID_NAME_HDR_CAPABILITIES "android.view.Display$HdrCapabilities" +#define FF_DISPLAY_ANDROID_NAME_FRAME_RATE_CATEGORY_RATE "android.view.FrameRateCategoryRate" + +// `IDisplayManager$Stub`, as the device's own dex declares it. `getDisplayInfo` is the first method +// and has been since the interface was introduced -- it is an `@UnsupportedAppUsage` API and that +// order has never moved -- and `getDisplayIds` is the second. They are not counted out of the AOSP +// source: a vendor fork inserts methods freely, and the seven a fork added on the test device all sit +// behind these two. +#define FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_INFO 1u +#define FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_IDS 2u + +// One display's reply is 2404 bytes on the test device, most of it the two mode lists, and it grows +// with the display. A reply that does not fit is a failure rather than a truncation. +#define FF_DISPLAY_ANDROID_REPLY_SIZE 8192 +#define FF_DISPLAY_ANDROID_PARCEL_SIZE 256 + +// How many displays are read at most. The reply buffer holds one display, so this only bounds the id +// list and the array the parsed records are held in until every one of them has parsed. +#define FF_DISPLAY_ANDROID_MAX_DISPLAYS 8 + +// How many fields each write sequence can hold. A sequence that does not fit is reported as empty +// rather than truncated, which fails the route -- half a sequence would walk half a record. See +// common/android/dex.h. +#define FF_DISPLAY_ANDROID_INFO_FIELDS 64 +#define FF_DISPLAY_ANDROID_NESTED_FIELDS 8 +#define FF_DISPLAY_ANDROID_MODE_FIELDS 12 + +// The write sequence of every class the reply is made of, read out of the device's own jar. It is +// what turns the parse from a sequence written against one release into a walk of the release that +// is running. +typedef struct FFDisplayAndroidLayout { + FFDexParcelField displayInfo[FF_DISPLAY_ANDROID_INFO_FIELDS]; + uint32_t displayInfoCount; + FFDexParcelField address[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t addressCount; + FFDexParcelField productInfo[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t productInfoCount; + FFDexParcelField manufactureDate[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t manufactureDateCount; + FFDexParcelField hdrCapabilities[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t hdrCapabilitiesCount; + FFDexParcelField mode[FF_DISPLAY_ANDROID_MODE_FIELDS]; + uint32_t modeCount; + FFDexParcelField frameRateCategoryRate[FF_DISPLAY_ANDROID_NESTED_FIELDS]; + uint32_t frameRateCategoryRateCount; + + // `DisplayCutout` is the one class whose body is not field-driven -- `writeCutoutToParcel` reads + // everything through compiler-synthesised accessors, so there is no `iget` to walk. What says how + // long its body is, is which fields the class declares: `mCutoutPathParserInfo` arrived in + // Android 13 and adds nine writes, `mSideOverrides` in 16 and adds one. + bool cutoutPathParserInfo; + bool cutoutSideOverrides; +} FFDisplayAndroidLayout; + +// Reads every sequence above out of the dex entries of `jarPath`, in one lookup. +// +// Every sequence but `FrameRateCategoryRate` is required: a class the jar does not declare leaves an +// empty sequence, and a walk without one has nothing to follow. `FrameRateCategoryRate` arrived in +// Android 15, so a jar that does not declare it is answering rather than failing -- the field is +// simply not in the sequence behind it, and the walk never asks. +// +// A jar that cannot be read at all fails too, and the route falls back to `cmd` / `dumpsys`. That is +// the point of the dex being the source: a build this parser cannot read gets the fallback, rather +// than a plausible number read out of the middle of a different field. +static bool displayLayoutLoad(const char* jarPath, FFDisplayAndroidLayout* layout) { + const FFDexParcelRequest requests[] = { + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_INFO, + .methodName = "writeToParcel", + .fields = layout->displayInfo, + .capacity = FF_DISPLAY_ANDROID_INFO_FIELDS, + .count = &layout->displayInfoCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_ADDRESS, + .methodName = "writeToParcel", + .fields = layout->address, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->addressCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_PRODUCT_INFO, + .methodName = "writeToParcel", + .fields = layout->productInfo, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->productInfoCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_MANUFACTURE_DATE, + .methodName = "writeToParcel", + .fields = layout->manufactureDate, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->manufactureDateCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_HDR_CAPABILITIES, + .methodName = "writeToParcel", + .fields = layout->hdrCapabilities, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->hdrCapabilitiesCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_MODE, + .methodName = "writeToParcel", + .fields = layout->mode, + .capacity = FF_DISPLAY_ANDROID_MODE_FIELDS, + .count = &layout->modeCount, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_FRAME_RATE_CATEGORY_RATE, + .methodName = "writeToParcel", + .fields = layout->frameRateCategoryRate, + .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, + .count = &layout->frameRateCategoryRateCount, + }, + }; + + const char* error = ffDexParcelFields(jarPath, requests, ARRAY_SIZE(requests)); + if (error != nullptr) { + FF_DEBUG("Reading the display layout out of %s failed: %s", jarPath, error); + return false; + } + if (layout->displayInfoCount == 0 || layout->addressCount == 0 || layout->productInfoCount == 0 + || layout->manufactureDateCount == 0 || layout->hdrCapabilitiesCount == 0 || layout->modeCount == 0) { + FF_DEBUG("The classes a display is made of are not all declared in %s", jarPath); + return false; + } + + const FFDexInstanceFieldRequest cutoutRequests[] = { + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_CUTOUT, + .fieldName = "mCutoutPathParserInfo", + .result = &layout->cutoutPathParserInfo, + }, + { + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_CUTOUT, + .fieldName = "mSideOverrides", + .result = &layout->cutoutSideOverrides, + }, + }; + error = ffDexInstanceFields(jarPath, cutoutRequests, ARRAY_SIZE(cutoutRequests)); + if (error != nullptr) { + FF_DEBUG("Reading the cutout layout out of %s failed: %s", jarPath, error); + return false; + } + + FF_DEBUG( + "Display layout: %u fields in DisplayInfo, %u in a mode, cutout path parser info %s, side overrides %s", + layout->displayInfoCount, layout->modeCount, + layout->cutoutPathParserInfo ? "yes" : "no", layout->cutoutSideOverrides ? "yes" : "no" + ); + return true; +} + +// `DisplayInfo.type`, i.e. `Display.TYPE_*`. The `cmd` route compares the same field as the text +// `INTERNAL` / `EXTERNAL` / `WIFI`; the numbers are what the parcel carries. +typedef enum FFDisplayAndroidType : int32_t { + FF_DISPLAY_ANDROID_TYPE_UNKNOWN = 0, + FF_DISPLAY_ANDROID_TYPE_INTERNAL = 1, + FF_DISPLAY_ANDROID_TYPE_EXTERNAL = 2, + FF_DISPLAY_ANDROID_TYPE_WIFI = 3, + FF_DISPLAY_ANDROID_TYPE_OVERLAY = 4, + FF_DISPLAY_ANDROID_TYPE_VIRTUAL = 5, +} FFDisplayAndroidType; + +// The `Parcel.writeValue` tags `DeviceProductInfo` uses. The framework writes one of these in front +// of the value; the numbers are its own. +typedef enum FFDisplayAndroidValue : int32_t { + FF_DISPLAY_ANDROID_VALUE_NULL = -1, + FF_DISPLAY_ANDROID_VALUE_STRING = 0, + FF_DISPLAY_ANDROID_VALUE_INTEGER = 1, + FF_DISPLAY_ANDROID_VALUE_PARCELABLE = 4, +} FFDisplayAndroidValue; + +// A cursor over one reply. Every read is bounds checked and every mismatch raises `failed` rather +// than returning a value: once a field has been read at the wrong place nothing behind it can be +// trusted, and the only safe answer left is the fallback. +typedef struct FFDisplayAndroidReader { + const uint8_t* data; + size_t size; + size_t offset; + bool failed; +} FFDisplayAndroidReader; + +static bool readerSkip(FFDisplayAndroidReader* reader, size_t bytes) { + if (bytes > reader->size - reader->offset) { + reader->failed = true; + return false; + } + reader->offset += bytes; + return true; +} + +static int32_t readerI32(FFDisplayAndroidReader* reader) { + if (reader->offset + sizeof(int32_t) > reader->size) { + reader->failed = true; + return 0; + } + int32_t value = 0; + memcpy(&value, reader->data + reader->offset, sizeof(int32_t)); + reader->offset += sizeof(int32_t); + return value; +} + +static uint32_t readerU32(FFDisplayAndroidReader* reader) { + return (uint32_t) readerI32(reader); +} + +static uint64_t readerU64(FFDisplayAndroidReader* reader) { + if (reader->offset + sizeof(uint64_t) > reader->size) { + reader->failed = true; + return 0; + } + uint64_t value = 0; + memcpy(&value, reader->data + reader->offset, sizeof(uint64_t)); + reader->offset += sizeof(uint64_t); + return value; +} + +static float readerF32(FFDisplayAndroidReader* reader) { + const uint32_t raw = readerU32(reader); + float value = 0; + memcpy(&value, &raw, sizeof(value)); + return value; +} + +// A `writeBoolean`, which is an int32. Anything but 0 or 1 is a field read at the wrong place. +static bool readerBool(FFDisplayAndroidReader* reader) { + const int32_t value = readerI32(reader); + if (value != 0 && value != 1) { + reader->failed = true; + return false; + } + return value == 1; +} + +// Whether the string16 at `offset` holds `expected`, which is a class name. The length has to be the +// name's own and every code unit has to spell it, so a body that happens to start with a plausible +// length does not pass for one. +static bool readerClassNameAt(const FFDisplayAndroidReader* reader, size_t offset, const char* expected) { + const size_t length = strlen(expected); + if (length > 128 || offset + sizeof(int32_t) + (length + 1) * 2 > reader->size) { + return false; + } + int32_t declared = 0; + memcpy(&declared, reader->data + offset, sizeof(int32_t)); + if ((size_t) declared != length) { + return false; + } + const uint8_t* text = reader->data + offset + sizeof(int32_t); + for (size_t i = 0; i < length; ++i) { + if (text[i * 2] != (uint8_t) expected[i] || text[i * 2 + 1] != 0) { + return false; + } + } + return true; +} + +// The class name in front of a Parcelable body, which `writeParcelable` writes and which is the +// anchor this whole parser hangs on: a field that moved puts something else here, and a name that +// does not decode is how the parser finds out instead of reading a length out of the middle of a +// body. +static bool readerClassName(FFDisplayAndroidReader* reader, const char* expected) { + if (!readerClassNameAt(reader, reader->offset, expected)) { + reader->failed = true; + return false; + } + return readerSkip(reader, sizeof(int32_t) + (((strlen(expected) + 1) * 2 + 3) & ~(size_t) 3)); +} + +// A `writeString`, which is UTF-16 despite the name: an int32 count of code units, the code units, a +// NUL terminator and the padding behind it. A count of -1 is how the same field spells a null String +// and is not an error; `result` may be null where the text is only in the way. +static bool readerString16(FFDisplayAndroidReader* reader, FFstrbuf* result) { + const int32_t length = readerI32(reader); + if (reader->failed) { + return false; + } + if (length < 0) { + return true; + } + const size_t payload = ((size_t) length * 2 + 2 + 3) & ~(size_t) 3; + if (payload > reader->size - reader->offset) { + reader->failed = true; + return false; + } + if (result != nullptr) { + for (int32_t i = 0; i < length; ++i) { + uint32_t codePoint = (uint32_t) reader->data[reader->offset + (size_t) i * 2] + | ((uint32_t) reader->data[reader->offset + (size_t) i * 2 + 1] << 8); + if (codePoint >= 0xD800 && codePoint <= 0xDBFF && i + 1 < length) { + // Anything outside the basic plane arrives as a surrogate pair, and the low half is + // what turns it back into one code point. A high surrogate without one is malformed + // and is passed through as it stands rather than guessed at. + const uint32_t low = (uint32_t) reader->data[reader->offset + (size_t) (i + 1) * 2] + | ((uint32_t) reader->data[reader->offset + (size_t) (i + 1) * 2 + 1] << 8); + if (low >= 0xDC00 && low <= 0xDFFF) { + codePoint = 0x10000u + ((codePoint - 0xD800u) << 10) + (low - 0xDC00u); + ++i; + } + } + ffStrbufAppendUtf32CodePoint(result, codePoint); + } + } + reader->offset += payload; + return true; +} + +// A `writeString8`, which is the one that really is UTF-8: an int32 *byte* count, that many bytes, a +// NUL and the padding. This is what `DisplayInfo.name` and `DisplayInfo.uniqueId` use -- the built-in +// display's name is 12 bytes of UTF-8 where the same text is 4 code units as a string16. +static bool readerString8(FFDisplayAndroidReader* reader, FFstrbuf* result) { + const int32_t length = readerI32(reader); + if (reader->failed) { + return false; + } + if (length < 0) { + return true; + } + const size_t payload = ((size_t) length + 1 + 3) & ~(size_t) 3; + if (payload > reader->size - reader->offset) { + reader->failed = true; + return false; + } + if (result != nullptr) { + ffStrbufAppendNS(result, (uint32_t) length, (const char*) reader->data + reader->offset); + } + reader->offset += payload; + return true; +} + +// An array of primitives: a count and the values. A count of -1 is a null array, which carries none. +static bool readerSkipArray(FFDisplayAndroidReader* reader, size_t elementSize) { + const int32_t count = readerI32(reader); + if (reader->failed) { + return false; + } + if (count < 0) { + return true; + } + if (count > 1024) { + reader->failed = true; + return false; + } + return readerSkip(reader, (size_t) count * elementSize); +} + +// A `writeValue` whose value is an Integer, or a null which reads as zero. +static int32_t readerValueInteger(FFDisplayAndroidReader* reader) { + switch ((FFDisplayAndroidValue) readerI32(reader)) { + case FF_DISPLAY_ANDROID_VALUE_NULL: + return 0; + case FF_DISPLAY_ANDROID_VALUE_INTEGER: + return readerI32(reader); + default: + reader->failed = true; + return 0; + } +} + +// A `writeValue` whose value is a String, an Integer or a null. Nothing here wants the value; the +// shapes it does not know are what it refuses to guess a length for. +static bool readerSkipValue(FFDisplayAndroidReader* reader) { + switch ((FFDisplayAndroidValue) readerI32(reader)) { + case FF_DISPLAY_ANDROID_VALUE_NULL: + return true; + case FF_DISPLAY_ANDROID_VALUE_STRING: + return readerString16(reader, nullptr); + case FF_DISPLAY_ANDROID_VALUE_INTEGER: + (void) readerI32(reader); + return !reader->failed; + default: + reader->failed = true; + return false; + } +} + +// One field whose dex type descriptor settles how it is framed. `[I` and `[F` are the two array +// shapes the record carries and report their element count; the rest report their value. +typedef struct FFDisplayAndroidScalar { + int32_t i32; + float f32; + int32_t count; +} FFDisplayAndroidScalar; + +// Reads one field whose descriptor settles its framing -- an `I`, an `F`, a `J`, a `Z`, an `[I` or +// an `[F`. A descriptor with no rule here is a failure rather than a guess: `Ljava/lang/String;` +// alone can be a `writeString` or a `writeString8`, the two are not the same length on the wire, and +// stepping over it as if it were either would move every field behind it. +static bool readerScalar(FFDisplayAndroidReader* reader, const char* type, FFDisplayAndroidScalar* value) { + *value = (FFDisplayAndroidScalar) {}; + if (ffStrEquals(type, "I")) { + value->i32 = readerI32(reader); + return !reader->failed; + } + if (ffStrEquals(type, "F")) { + value->f32 = readerF32(reader); + return !reader->failed; + } + if (ffStrEquals(type, "J")) { + (void) readerU64(reader); + return !reader->failed; + } + if (ffStrEquals(type, "Z")) { + value->i32 = readerBool(reader) ? 1 : 0; + return !reader->failed; + } + if (ffStrEquals(type, "[I") || ffStrEquals(type, "[F")) { + const int32_t count = readerI32(reader); + if (reader->failed || count < 0 || count > 1024) { + reader->failed = true; + return false; + } + value->count = count; + return readerSkip(reader, (size_t) count * (ffStrEquals(type, "[I") ? sizeof(int32_t) : sizeof(float))); + } + reader->failed = true; + return false; +} + +// The class name in front of a nested body, checked against the class the field is expected to carry. +// `writeParcelable` writes the runtime class, so the abstract `DisplayAddress` a `DisplayInfo` +// declares arrives as its `$Physical` subclass; a null field is a single -1 where the class name +// would be, which is a value and not a failure. +static bool readerParcelableHead(FFDisplayAndroidReader* reader, const char* expected, bool* present) { + const int32_t length = readerI32(reader); + if (reader->failed) { + return false; + } + if (length < 0) { + *present = false; + return true; + } + reader->offset -= sizeof(int32_t); + *present = true; + return readerClassName(reader, expected); +} + +// A `Rect` written by `writeTypedObject` or by `writeTypedArray`: the non-null marker the typed write +// puts in front of the body and then four ints. A null is a marker and no ints behind it. +static bool readerRect(FFDisplayAndroidReader* reader) { + const int32_t marker = readerI32(reader); + if (reader->failed) { + return false; + } + return marker < 0 || readerSkip(reader, 4 * sizeof(int32_t)); +} + +// `writeValue(mManufactureDate)`, the only Parcelable a `writeValue` carries here, and the one place +// the parcel does something the AOSP sources do not describe: on Android 13 and later the tag is +// followed by the size of everything behind it -- the class name and the body -- before the class +// name, which is why a plain `tag + class name` walk lands four bytes early; Android 11 has no size +// at all. Neither layout is assumed -- a value that decodes as the class name has no size, anything +// else is stepped over -- and where there is one the cursor is snapped to the end it promises, so a +// body this parser reads differently can not move the fields behind it. The size counts from its own +// end, not from its start. +static bool readerManufactureDate(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, uint16_t* year, uint16_t* week) { + if ((FFDisplayAndroidValue) readerI32(reader) != FF_DISPLAY_ANDROID_VALUE_PARCELABLE) { + reader->failed = true; + return false; + } + + size_t bodyEnd = 0; + if (!readerClassNameAt(reader, reader->offset, FF_DISPLAY_ANDROID_NAME_MANUFACTURE_DATE)) { + const int32_t declared = readerI32(reader); + if (declared <= 0) { + reader->failed = true; + return false; + } + bodyEnd = reader->offset + (size_t) declared; + } + if (!readerClassName(reader, FF_DISPLAY_ANDROID_NAME_MANUFACTURE_DATE)) { + return false; + } + + // `writeValue(mWeek)` and `writeValue(mYear)`, each an Integer or a null. Nothing is reported + // unless both are there, so a class that stops writing one leaves the date at nothing rather + // than at half of it. + uint16_t weekValue = 0; + uint16_t yearValue = 0; + for (uint32_t i = 0; i < layout->manufactureDateCount; ++i) { + const int32_t value = readerValueInteger(reader); + if (reader->failed || value < 0 || value > 3000) { + reader->failed = true; + return false; + } + if (ffStrEquals(layout->manufactureDate[i].name, "mWeek")) { + if (value > 53) { + reader->failed = true; + return false; + } + weekValue = (uint16_t) value; + } else if (ffStrEquals(layout->manufactureDate[i].name, "mYear")) { + yearValue = (uint16_t) value; + } + } + *week = weekValue; + *year = yearValue; + + if (bodyEnd > 0) { + if (bodyEnd < reader->offset || bodyEnd > reader->size) { + reader->failed = true; + return false; + } + reader->offset = bodyEnd; + } + return true; +} + +// `deviceProductInfo`, which is where a display's manufacture date is carried. Nothing else in it is +// used, but it sits in front of `name`, the density and the rotation, so it has to be walked whether +// or not its values are wanted -- and it is null on some displays. +// +// Three of its six fields are handed to `writeValue`, which frames them by their runtime type rather +// than by their declared one: `mProductId` is declared a `String` and goes through `writeValue` where +// `mName` next to it goes through `writeString`, so those are named rather than dispatched by type. +// The sixth is `mRelativeAddress`, an int array, on Android 11 and `mConnectionToSinkType`, an int, +// on 13 and later; the descriptor settles that one, which is what makes the two read the same code. +static bool readerDeviceProductInfo(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, uint16_t* year, uint16_t* week) { + bool present = false; + if (!readerParcelableHead(reader, FF_DISPLAY_ANDROID_NAME_PRODUCT_INFO, &present)) { + return false; + } + if (!present) { + return true; + } + + for (uint32_t i = 0; i < layout->productInfoCount; ++i) { + const FFDexParcelField* field = &layout->productInfo[i]; + if (ffStrEquals(field->name, "mName") || ffStrEquals(field->name, "mManufacturerPnpId")) { + if (!readerString16(reader, nullptr)) { + return false; + } + } else if (ffStrEquals(field->name, "mProductId") || ffStrEquals(field->name, "mModelYear")) { + if (!readerSkipValue(reader)) { + return false; + } + } else if (ffStrEquals(field->name, "mManufactureDate")) { + if (!readerManufactureDate(reader, layout, year, week)) { + return false; + } + } else { + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, field->type, &value)) { + return false; + } + } + } + return true; +} + +// `address`, which is null on some displays and a `DisplayAddress$Physical` on others. Its body is +// one `long` on every release seen -- the older sources describe a `long` plus an `int`, and that +// shape has not turned up on any of the three jars -- and it is walked out of the jar's own sequence +// rather than assumed, so a release that changes it is read rather than misread. +static bool readerDisplayAddress(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout) { + bool present = false; + if (!readerParcelableHead(reader, FF_DISPLAY_ANDROID_NAME_ADDRESS, &present)) { + return false; + } + if (!present) { + return true; + } + for (uint32_t i = 0; i < layout->addressCount; ++i) { + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, layout->address[i].type, &value)) { + return false; + } + } + return true; +} + +// A nested Parcelable whose values this module does not use, walked because the fields behind it sit +// on the far side of it. `frameRateCategoryRate` is the only one of those left before `uniqueId`. +static bool readerSkippedParcelable(FFDisplayAndroidReader* reader, const char* expected, const FFDexParcelField* fields, uint32_t count) { + bool present = false; + if (!readerParcelableHead(reader, expected, &present)) { + return false; + } + if (!present) { + return true; + } + for (uint32_t i = 0; i < count; ++i) { + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, fields[i].type, &value)) { + return false; + } + } + return true; +} + +// `hdrCapabilities`, whose only value this module uses is how many HDR types the display supports -- +// an empty list is the display saying it can not do HDR at all. +static bool readerHdrCapabilities(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, int32_t* hdrTypeCount) { + bool present = false; + if (!readerParcelableHead(reader, FF_DISPLAY_ANDROID_NAME_HDR_CAPABILITIES, &present)) { + return false; + } + if (!present) { + return true; + } + for (uint32_t i = 0; i < layout->hdrCapabilitiesCount; ++i) { + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, layout->hdrCapabilities[i].type, &value)) { + return false; + } + if (ffStrEquals(layout->hdrCapabilities[i].name, "mSupportedHdrTypes")) { + if (value.count > 16) { + reader->failed = true; + return false; + } + *hdrTypeCount = value.count; + } + } + return true; +} + +// `displayCutout`, the largest nested structure in the record and the one that is easiest to get +// wrong: it is serialized by `DisplayCutout$ParcelableWrapper.writeCutoutToParcel`, because +// `DisplayCutout` has no `writeToParcel` of its own, and that method reads everything through +// `-$$Nest$` accessors the compiler synthesised -- so there is no `iget` to walk and no class name on +// the wire either, and the write sequence the jar gives for every other class says nothing here. +// +// What says how long the body is, is which fields the class declares. Android 11 declares three -- +// `mBounds`, `mSafeInsets`, `mWaterfallInsets` -- Android 13 adds `mCutoutPathParserInfo`, which is +// nine more writes, and Android 16 adds `mSideOverrides`, which is one more. Both are read from the +// jar rather than from a version number. None of it is reported; it is walked because `rotation` and +// everything behind it sit on the far side of it. +static bool readerDisplayCutout(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout) { + const int32_t marker = readerI32(reader); + if (reader->failed) { + return false; + } + if (marker != 1) { + // 0 for a null cutout and -1 for `DisplayCutout.NO_CUTOUT`, which is a display with none at + // all. Anything else is a field read at the wrong place. + if (marker == 0 || marker == -1) { + return true; + } + reader->failed = true; + return false; + } + + if (!readerRect(reader)) { // mSafeInsets + return false; + } + const int32_t bounds = readerI32(reader); // mBounds, a typed array of Rects + if (reader->failed || bounds < 0 || bounds > 64) { + reader->failed = true; + return false; + } + for (int32_t i = 0; i < bounds; ++i) { + if (!readerRect(reader)) { + return false; + } + } + if (!readerRect(reader)) { // mWaterfallInsets + return false; + } + + if (layout->cutoutPathParserInfo) { + // the display size the cutout was built for and then the physical one + if (!readerSkip(reader, 4 * sizeof(int32_t))) { + return false; + } + const float density = readerF32(reader); + if (reader->failed || !(density > 0) || density > 1000) { + reader->failed = true; + return false; + } + // the cutout spec, e.g. `M -45,0 L -45,142 L 45,142 L 45,0 Z`, and then the rotation, the + // scale and the physical pixel ratio, which are three plain four byte fields + if (!readerString16(reader, nullptr) || !readerSkip(reader, 3 * sizeof(int32_t))) { + return false; + } + } + if (layout->cutoutSideOverrides) { + return readerSkipArray(reader, sizeof(int32_t)); // mSideOverrides + } + return true; +} + +// One entry of `supportedModes`, which is `Display$Mode`. +typedef struct FFDisplayAndroidMode { + int32_t id; + uint32_t width; + uint32_t height; + float refreshRate; +} FFDisplayAndroidMode; + +// Walks a `Display$Mode[]`: a count and then that many bare bodies, with no non-null marker per +// element, which is what `writeTypedArray` would put there -- the framework writes the length with +// `writeInt` and then one `writeToParcel` per element. The two ids the record carries are looked up +// as it goes, so the list does not have to be kept. +// +// The body is walked out of the jar's own sequence rather than read as a fixed run: `Display$Mode` +// writes four fields on Android 11, five on 13 and eight on 16, and the fourth -- the one the +// refresh rate is in -- is `mRefreshRate` on the first two and `mPeakRefreshRate` on the third. +static bool readerModeList(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, int32_t activeId, int32_t defaultId, FFDisplayAndroidMode* active, FFDisplayAndroidMode* preferred) { + const int32_t count = readerI32(reader); + if (reader->failed || count < 0 || count > 64) { + reader->failed = true; + return false; + } + for (int32_t i = 0; i < count; ++i) { + FFDisplayAndroidMode mode = {}; + for (uint32_t j = 0; j < layout->modeCount; ++j) { + const FFDexParcelField* field = &layout->mode[j]; + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, field->type, &value)) { + return false; + } + if (ffStrEquals(field->name, "mModeId")) { + mode.id = value.i32; + } else if (ffStrEquals(field->name, "mWidth")) { + mode.width = (uint32_t) value.i32; + } else if (ffStrEquals(field->name, "mHeight")) { + mode.height = (uint32_t) value.i32; + } else if (ffStrEquals(field->name, "mRefreshRate") || ffStrEquals(field->name, "mPeakRefreshRate")) { + mode.refreshRate = value.f32; + } + } + if (reader->failed + || mode.id < 0 || mode.width == 0 || mode.width > 32768 || mode.height == 0 || mode.height > 32768 + || !(mode.refreshRate > 0) || mode.refreshRate > 1000) { + reader->failed = true; + return false; + } + if (active != nullptr && mode.id == activeId) { + *active = mode; + } + if (preferred != nullptr && mode.id == defaultId) { + *preferred = mode; + } + } + return !reader->failed; +} + +// One parsed record. It is held until every display has parsed, because a record this parser does not +// recognise says the layout is not the one it was written for, and half an answer from a layout it +// does not understand is worse than falling back. +typedef struct FFDisplayAndroidInfo { + uint32_t width; + uint32_t height; + uint32_t preferredWidth; + uint32_t preferredHeight; + double preferredRefreshRate; + double refreshRate; + uint32_t density; + double physicalXDpi; + double physicalYDpi; + uint32_t rotation; + uint32_t displayId; + int32_t type; + int32_t modeId; + int32_t defaultModeId; + int32_t hdrTypeCount; + float renderFrameRate; + bool forceSdr; + uint16_t manufactureYear; + uint16_t manufactureWeek; + uint64_t id; + FFstrbuf name; +} FFDisplayAndroidInfo; + +// One `DisplayInfo` walk in progress: the record being filled, the two modes the mode list is +// resolved against, and the id string -- which is not part of the record, it is only read to pull the +// display's id out of it, so it is held here rather than there. +typedef struct FFDisplayAndroidWalk { + FFDisplayAndroidInfo* info; + FFstrbuf* uniqueId; + FFDisplayAndroidMode active; + FFDisplayAndroidMode preferred; +} FFDisplayAndroidWalk; + +// `uniqueId` identifies the display across reboots, e.g. `local:4630946557703207059` for the panel +// and `virtual:...` for a virtual one. The number behind the colon is what the `cmd` route extracts +// from the same string, so the two routes report the same id. +static uint64_t displayUniqueId(const char* uniqueId) { + const char* colon = strchr(uniqueId, ':'); + return (uint64_t) strtoull(colon != nullptr ? colon + 1 : uniqueId, nullptr, 10); +} + +// One field of the `DisplayInfo` write sequence. The name says which value the field carries; the +// type descriptor says how it is framed, and settles that for every primitive and both array shapes. +// +// Four things the descriptor does not settle, and each is named: +// +// * `displayCutout`, which has no write sequence to follow at all. See readerDisplayCutout. +// * `frameRateCategoryRate`, a Parcelable whose values this module does not use. +// * `supportedModes` and `appsSupportedModes`, arrays of Parcelables written by a loop rather than +// by a `write*Array`, so the shape behind them is `Display$Mode`'s own. +// * The `String` fields. `DisplayInfo` writes its own through `writeString8` -- a byte count and +// UTF-8 -- while `DeviceProductInfo` writes its through `writeString`, which is a code-unit count +// and UTF-16; the descriptor is `Ljava/lang/String;` either way. Within one class the framing is +// the class's habit rather than the field's, so a `String` this does not know is read the way the +// ones it does are, which is what a field a later release adds gets. +// +// A *name* this does not know is stepped over by its type, which is what makes a field a later +// release added cost its bytes and nothing else -- `displayGroupId`, `renderFrameRate`, +// `hasArrSupport`, `supportedRefreshRates`, `userPreferredModeId`, `appsSupportedModes`, `isForceSdr` +// and `committedState` are all of them. A *type* this does not know is a failure: the descriptor is +// the only thing that says how long the field is, and a `String` alone can be two different lengths. +static bool displayInfoField(FFDisplayAndroidReader* reader, const FFDexParcelField* field, const FFDisplayAndroidLayout* layout, FFDisplayAndroidWalk* walk) { + FFDisplayAndroidInfo* info = walk->info; + const char* name = field->name; + + if (ffStrEquals(name, "address")) { + return readerDisplayAddress(reader, layout); + } + if (ffStrEquals(name, "deviceProductInfo")) { + return readerDeviceProductInfo(reader, layout, &info->manufactureYear, &info->manufactureWeek); + } + if (ffStrEquals(name, "displayCutout")) { + return readerDisplayCutout(reader, layout); + } + if (ffStrEquals(name, "hdrCapabilities")) { + return readerHdrCapabilities(reader, layout, &info->hdrTypeCount); + } + if (ffStrEquals(name, "frameRateCategoryRate")) { + return readerSkippedParcelable(reader, FF_DISPLAY_ANDROID_NAME_FRAME_RATE_CATEGORY_RATE, layout->frameRateCategoryRate, layout->frameRateCategoryRateCount); + } + if (ffStrEquals(name, "supportedModes") || ffStrEquals(name, "appsSupportedModes")) { + // The two ids the list is resolved against are written in front of it on every release seen. + // One that moved behind it would leave the list resolved against nothing, so the walk says so + // rather than reporting the mode of whatever id happens to sit at the sentinel. + const bool wanted = ffStrEquals(name, "supportedModes"); + if (wanted && (info->modeId == INT32_MIN || info->defaultModeId == INT32_MIN)) { + reader->failed = true; + return false; + } + return readerModeList(reader, layout, info->modeId, info->defaultModeId, + wanted ? &walk->active : nullptr, wanted ? &walk->preferred : nullptr); + } + if (ffStrEquals(field->type, "Ljava/lang/String;")) { + if (ffStrEquals(name, "name")) { + return readerString8(reader, &info->name); + } + if (ffStrEquals(name, "uniqueId")) { + return readerString8(reader, walk->uniqueId); + } + return readerString8(reader, nullptr); // `ownerPackageName`, and anything a later release adds + } + + FFDisplayAndroidScalar value = {}; + if (!readerScalar(reader, field->type, &value)) { + return false; + } + if (ffStrEquals(name, "type")) { + info->type = value.i32; + } else if (ffStrEquals(name, "displayId")) { + info->displayId = (uint32_t) value.i32; + } else if (ffStrEquals(name, "logicalWidth")) { + info->width = (uint32_t) value.i32; + } else if (ffStrEquals(name, "logicalHeight")) { + info->height = (uint32_t) value.i32; + } else if (ffStrEquals(name, "rotation")) { + info->rotation = (uint32_t) value.i32; + } else if (ffStrEquals(name, "modeId")) { + info->modeId = value.i32; + } else if (ffStrEquals(name, "defaultModeId")) { + info->defaultModeId = value.i32; + } else if (ffStrEquals(name, "renderFrameRate")) { + if (!(value.f32 > 0) || value.f32 > 1000) { + reader->failed = true; + return false; + } + info->renderFrameRate = value.f32; + } else if (ffStrEquals(name, "logicalDensityDpi")) { + info->density = (uint32_t) value.i32; + } else if (ffStrEquals(name, "physicalXDpi")) { + info->physicalXDpi = value.f32; + } else if (ffStrEquals(name, "physicalYDpi")) { + info->physicalYDpi = value.f32; + } else if (ffStrEquals(name, "isForceSdr")) { + info->forceSdr = value.i32 != 0; + } + return true; +} + +// One `getDisplayInfo` reply: the exception code the stub writes, the non-null marker +// `writeTypedObject` writes in front of the Parcelable, and the record itself -- walked in the order +// the device's own `DisplayInfo.writeToParcel` writes it, out of the sequence `displayLayoutLoad` +// read from that device's jar. +static bool displayInfoParse(const uint8_t* data, size_t size, const FFDisplayAndroidLayout* layout, FFDisplayAndroidInfo* info) { + FFDisplayAndroidReader reader = { .data = data, .size = size, .offset = 0, .failed = false }; + + const int32_t exception = readerI32(&reader); + if (exception != 0) { + FF_DEBUG("getDisplayInfo raised exception %d", exception); + return false; + } + if (readerI32(&reader) != 1) { + FF_DEBUG("getDisplayInfo answered no display"); + return false; + } + + // Sentinels, so a field the release does not write can not be mistaken for one that was read as + // zero. Every one of these is in the sequence on every release seen and the check below is what + // says so; the two that are not -- `renderFrameRate` and `isForceSdr` -- have a defined fallback + // and are the only ones left out of it. + info->type = -1; + info->displayId = UINT32_MAX; + info->width = 0; + info->height = 0; + info->rotation = UINT32_MAX; + info->modeId = INT32_MIN; + info->defaultModeId = INT32_MIN; + info->hdrTypeCount = 0; + info->renderFrameRate = 0; + info->density = 0; + info->physicalXDpi = 0; + info->physicalYDpi = 0; + info->forceSdr = false; + + FF_STRBUF_AUTO_DESTROY uniqueId = ffStrbufCreate(); + FFDisplayAndroidWalk walk = { .info = info, .uniqueId = &uniqueId }; + + // `uniqueId` is the last field this module reads. Everything behind it -- `removeMode`, the + // brightness, the corners and shapes, the throttling -- would have to be walked for nothing, so + // the walk stops here rather than implementing four more nested shapes no value comes out of. + bool complete = false; + for (uint32_t i = 0; i < layout->displayInfoCount; ++i) { + const FFDexParcelField* field = &layout->displayInfo[i]; + if (!displayInfoField(&reader, field, layout, &walk)) { + FF_DEBUG("getDisplayInfo(%u) answered a %s this parser does not recognise", info->displayId, field->name); + return false; + } + if (ffStrEquals(field->name, "uniqueId")) { + complete = true; + break; + } + } + if (!complete) { + FF_DEBUG("getDisplayInfo(%u) answered a record with no uniqueId", info->displayId); + return false; + } + + if (info->type < 0 || info->type > FF_DISPLAY_ANDROID_TYPE_VIRTUAL + || info->displayId == UINT32_MAX + || info->width == 0 || info->width > 65536 || info->height == 0 || info->height > 65536 + || info->rotation > 3 + || info->modeId < 0 || info->defaultModeId < -1 + || info->density > 4000 + || !(info->physicalXDpi >= 0) || info->physicalXDpi > 2000 + || !(info->physicalYDpi >= 0) || info->physicalYDpi > 2000 + || info->name.length == 0 || info->name.length > 256 + || uniqueId.length > 256) { + FF_DEBUG( + "getDisplayInfo(%u) answered type %d, %u x %u, rotation %u, mode %d, density %u, %f x %f dpi", + info->displayId, info->type, info->width, info->height, info->rotation, info->modeId, info->density, + info->physicalXDpi, info->physicalYDpi + ); + return false; + } + + info->preferredWidth = walk.preferred.width; + info->preferredHeight = walk.preferred.height; + info->preferredRefreshRate = walk.preferred.refreshRate; + // The nominal rate of the active mode, which is what `Display.getRefreshRate()` reports and what + // every other platform reports. `renderFrameRate` is only used when the list has no entry for + // `modeId`, because it is a render cadence that follows the content rather than a property of the + // display -- and it is not written at all before Android 15. `refreshRateOverride` is deliberately + // not consulted either: the `cmd` route can not see it, and the two routes have to answer the same + // thing. + info->refreshRate = walk.active.refreshRate > 0 ? walk.active.refreshRate : info->renderFrameRate; + info->id = displayUniqueId(uniqueId.chars); + return true; +} + +static FFDisplayType displayType(int32_t type) { + switch ((FFDisplayAndroidType) type) { + case FF_DISPLAY_ANDROID_TYPE_INTERNAL: + return FF_DISPLAY_TYPE_BUILTIN; + case FF_DISPLAY_ANDROID_TYPE_EXTERNAL: + case FF_DISPLAY_ANDROID_TYPE_WIFI: + // A WIFI display is a wireless sink, which is as external as a wired one + return FF_DISPLAY_TYPE_EXTERNAL; + default: + return FF_DISPLAY_TYPE_UNKNOWN; + } +} + +// `hdrCapabilities.mSupportedHdrTypes` is the display's own answer and an empty list means it can not +// do HDR at all. `isForceSdr` is the framework having turned every capability of this display off and +// `persist.sys.hdr_mode` is the vendor's switch; both are what the `cmd` route consults, and both are +// consulted here so that the two routes answer the same thing. +static FFDisplayHdrStatus displayHdrStatus(const FFDisplayAndroidInfo* info) { + if (info->hdrTypeCount <= 0) { + return FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } + FF_STRBUF_AUTO_DESTROY hdrMode = ffStrbufCreate(); + if (!info->forceSdr && ffSettingsGetAndroidProperty("persist.sys.hdr_mode", &hdrMode) && ffStrbufToUInt(&hdrMode, 0) > 0) { + return FF_DISPLAY_HDR_STATUS_ENABLED; + } + return FF_DISPLAY_HDR_STATUS_SUPPORTED; +} + +// Reads every enabled display over binder. Returns false without appending anything when the service, +// the transaction or the layout is not what this parser was written for, which is what hands the +// answer to `cmd` and `dumpsys` behind it. +static bool detectWithBinder(FFDisplayServerResult* ds) { + // The write sequence first. It is what the whole parse is made of, and a build whose jar cannot be + // read has nothing to walk -- so failing here costs one file open rather than a round trip to a + // service whose answer could not be read anyway. + FFDisplayAndroidLayout layout = {}; + if (!displayLayoutLoad(FF_DISPLAY_ANDROID_JAR, &layout)) { + return false; + } + + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; + const char* error = ffBinderOpen(&binder); + if (error != nullptr) { + FF_DEBUG("Reading the display over binder failed: %s", error); + return false; + } + + // Released on every path out, including the early returns below. + [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder }; + error = ffBinderLookupService(&binder, FF_DISPLAY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle); + if (error != nullptr) { + FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_DISPLAY_ANDROID_SERVICE, error); + return false; + } + + uint8_t parcelBuffer[FF_DISPLAY_ANDROID_PARCEL_SIZE]; + uint8_t replyBuffer[FF_DISPLAY_ANDROID_REPLY_SIZE]; + + // `getDisplayIds(false)` is the list of the enabled displays, which is the set `cmd display + // get-displays` prints. It is a separate call rather than a loop over 0..n because the ids are not + // contiguous, and it is twelve bytes. + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_DISPLAY_ANDROID_DESCRIPTOR); + ffBinderParcelPutI32(&parcel, 0); // includeDisabled + + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + error = ffBinderTransact(&binder, service.handle, FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_IDS, 0, &parcel, &reply); + if (error != nullptr) { + FF_DEBUG("getDisplayIds failed: %s", error); + return false; + } + if (ffBinderReplyIsStatus(&reply)) { + FF_DEBUG("getDisplayIds answered status %d instead of a parcel", (int) ffBinderReadI32(reply.data, reply.size, 0)); + return false; + } + const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0); + if (exception != 0) { + FF_DEBUG("getDisplayIds raised exception %d", exception); + return false; + } + const int32_t count = ffBinderReadI32(reply.data, reply.size, sizeof(int32_t)); + if (count <= 0 || count > FF_DISPLAY_ANDROID_MAX_DISPLAYS) { + FF_DEBUG("getDisplayIds answered %d displays", count); + return false; + } + + int32_t displayIds[FF_DISPLAY_ANDROID_MAX_DISPLAYS] = {}; + for (int32_t i = 0; i < count; ++i) { + displayIds[i] = ffBinderReadI32(reply.data, reply.size, (size_t) (2 + i) * sizeof(int32_t)); + if (displayIds[i] < 0) { + FF_DEBUG("getDisplayIds answered display id %d", displayIds[i]); + return false; + } + } + + // Every display has to parse before any of them is reported, so the records are held here until + // the loop has been through all of them. + FFDisplayAndroidInfo infos[FF_DISPLAY_ANDROID_MAX_DISPLAYS] = {}; + int32_t parsed = 0; + for (; parsed < count; ++parsed) { + FFDisplayAndroidInfo* info = &infos[parsed]; + ffStrbufInit(&info->name); + + FFBinderParcel request = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&request, FF_DISPLAY_ANDROID_DESCRIPTOR); + ffBinderParcelPutI32(&request, displayIds[parsed]); + + reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + error = ffBinderTransact(&binder, service.handle, FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_INFO, 0, &request, &reply); + if (error != nullptr) { + FF_DEBUG("getDisplayInfo(%d) failed: %s", displayIds[parsed], error); + break; + } + if (ffBinderReplyIsStatus(&reply)) { + FF_DEBUG("getDisplayInfo(%d) answered status %d instead of a parcel", displayIds[parsed], (int) ffBinderReadI32(reply.data, reply.size, 0)); + break; + } + if (!displayInfoParse(reply.data, reply.size, &layout, info)) { + FF_DEBUG("getDisplayInfo(%d) answered a record this parser does not recognise", displayIds[parsed]); + break; + } + } + + if (parsed != count) { + for (int32_t i = 0; i <= parsed && i < count; ++i) { + ffStrbufDestroy(&infos[i].name); + } + return false; + } + + for (int32_t i = 0; i < count; ++i) { + FFDisplayAndroidInfo* info = &infos[i]; + + // The physical dpi describes the panel itself and does not change with the logical display + // size, so the physical size has to be derived from the native resolution -- the same + // derivation the `cmd` route makes. + uint32_t physicalWidth = 0, physicalHeight = 0; + if (info->physicalXDpi > 0) { + physicalWidth = (uint32_t) ((info->preferredWidth ? info->preferredWidth : info->width) * 25.4 / info->physicalXDpi + 0.5); + } + if (info->physicalYDpi > 0) { + physicalHeight = (uint32_t) ((info->preferredHeight ? info->preferredHeight : info->height) * 25.4 / info->physicalYDpi + 0.5); + } + + FFDisplayResult* display = ffdsAppendDisplay(ds, + info->width, + info->height, + info->refreshRate, + info->density * 96 / 160, + info->preferredWidth, + info->preferredHeight, + info->preferredRefreshRate, + info->rotation, + &info->name, // moved + displayType(info->type), + info->displayId == 0, // Display 0 is the default one + info->id, + physicalWidth, + physicalHeight, + "binder"); + if (display == nullptr) { + continue; // a record with no size, which the parser above does not let through + } + display->manufactureYear = info->manufactureYear; + display->manufactureWeek = info->manufactureWeek; + display->hdrStatus = displayHdrStatus(info); + } + return ds->displays.length > 0; +} + // Several vendors embed the UI name and its version in `ro.build.display.id` without any // separator, e.g. `MyOS12.0.14_A2121` or `RedMagicOS10.0.24_NX779J`. static bool detectDEFromDisplayId(FFDisplayServerResult* ds, const char* const* names, uint32_t count) { @@ -672,11 +1865,12 @@ void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER); - // `cmd` comes first because it needs no permission and therefore also works for an app UID. - // `dumpsys` is the only route that answers on Android 12 and older, and only for `adb shell` and - // root -- it is skipped without a fork for every other UID, see detectWithDumpsys -- and `getprop` - // is MiUI specific and the last resort. - if (!detectWithCmd(ds) && !detectWithDumpsys(ds)) { + // Binder comes first: it needs no permission and no child process, and it is the only route that + // answers for an app UID on Android 12 and older. `cmd` is behind it for a record whose parcel + // layout is not the one the parser above knows, `dumpsys` for the shell and root -- it is the only + // route that answers at all on Android 12 and older, and it is skipped without a fork for every + // other UID, see detectWithDumpsys -- and `getprop` is MiUI specific and the last resort. + if (!detectWithBinder(ds) && !detectWithCmd(ds) && !detectWithDumpsys(ds)) { detectWithGetprop(ds); }