diff --git a/src/common/android/dex.c b/src/common/android/dex.c index cd935e84e..da18beb71 100644 --- a/src/common/android/dex.c +++ b/src/common/android/dex.c @@ -698,6 +698,46 @@ static const char* dexWalkParcelFields(const FFDexTables* tables, const uint8_t* return nullptr; } +// Walks the instance field list of one `class_data_item` and writes `true` for every name it holds. +// The list is the caller's, so this answers one request's worth of names or several. +// +// 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. It is asked as part of a write-sequence request rather +// than by a call of its own -- the list is already positioned by the `class_data_item` the sequence +// comes out of, and a separate call would repeat the whole walk to reach it. +static const char* dexMatchInstanceFields(const FFDexTables* tables, const uint8_t* fields, uint32_t fieldCount, const char* const* names, bool* results, uint32_t nameCount) { + const uint8_t* dex = tables->dex; + const uint8_t* end = tables->end; + + uint32_t fieldIndex = 0; + for (uint32_t j = 0; j < fieldCount; ++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 i = 0; i < nameCount; ++i) { + if (!results[i] && strcmp(name, names[i]) == 0) { + results[i] = true; + } + } + } + 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. @@ -721,6 +761,22 @@ static const char* dexClassParcelFields(const FFDexTables* tables, uint32_t type return error; } + // The instance fields first, out of the list the `class_data_item` has already positioned. A + // request that asked for them is answered here whether or not it also asked for a sequence, and + // one that asked for neither is settled by the loop at the end like any other. + for (uint32_t r = 0; r < count; ++r) { + if (settled[r] || typeIndexes[r] != typeIndex || requests[r].instanceFieldCount == 0) { + continue; + } + error = dexMatchInstanceFields( + tables, classData.instanceFields, classData.instanceFieldCount, + requests[r].instanceFieldNames, requests[r].instanceFieldResults, requests[r].instanceFieldCount + ); + 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. @@ -748,7 +804,10 @@ static const char* dexClassParcelFields(const FFDexTables* tables, uint32_t type } for (uint32_t r = 0; r < count; ++r) { - if (settled[r] || typeIndexes[r] != typeIndex || strcmp(name, requests[r].methodName) != 0) { + // A request that left the sequence out is one that only wanted the instance fields, + // which are already written; it has no method name to match against. + if (settled[r] || typeIndexes[r] != typeIndex || requests[r].methodName == nullptr + || strcmp(name, requests[r].methodName) != 0) { continue; } // A method with no body -- abstract, or native -- writes nothing, and the count stays 0. @@ -777,66 +836,6 @@ static const char* dexClassParcelFields(const FFDexTables* tables, uint32_t type 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; - } - - 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; -} - // --------------------------------------------------------------------------------------------- // Locating classes.dex // --------------------------------------------------------------------------------------------- @@ -1215,10 +1214,17 @@ const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* re } 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. + // Every request is written its answers before anything can fail, so that a caller reads an answer + // rather than whatever its own stack held there, however this returns. The instance fields go + // first: `false` is what both "the class does not declare it" and "the jar cannot be read" mean, + // and the walk below only ever turns one of them true. for (uint32_t r = 0; r < count; ++r) { - *requests[r].count = 0; + if (requests[r].methodName != nullptr) { + *requests[r].count = 0; + } + for (uint32_t i = 0; i < requests[r].instanceFieldCount; ++i) { + requests[r].instanceFieldResults[i] = false; + } } if (count == 0) { return nullptr; @@ -1287,88 +1293,16 @@ const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* req // 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". + // not tell "this build has no such method" apart from "the reader went wrong". The same goes for + // an instance field left `false`. for (uint32_t r = 0; r < count; ++r) { - if (*requests[r].count == 0) { + if (requests[r].methodName != nullptr && *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; + for (uint32_t i = 0; i < requests[r].instanceFieldCount; ++i) { + if (!requests[r].instanceFieldResults[i]) { + FF_DEBUG("\"%s\" is not an instance field of \"%s\"", requests[r].instanceFieldNames[i], requests[r].classDescriptor); } - 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 75ca4acb4..9f0b93ed2 100644 --- a/src/common/android/dex.h +++ b/src/common/android/dex.h @@ -122,42 +122,46 @@ typedef struct FFDexParcelField { char type[FF_DEX_PARCEL_FIELD_TYPE_MAX]; } FFDexParcelField; -// One `.` write sequence, and where it is written. +// One class, and the two things that can be asked about it: the write sequence of one of its methods, +// and which instance fields it declares. Either half may be left out, and both are answered out of +// the same `class_data_item` -- which is why they are one request rather than two calls. +// +// Asking for them together is not a convenience. Both halves are read from a walk that is dominated +// by the type table -- 8428 descriptors in `framework.jar`'s `classes.dex`, each read from a +// different part of a 51 MB mapping -- and the fields a class declares are listed in the very +// `class_data_item` the sequence is read out of. A second call would map the jar again, open every +// entry again and make that pass again to reach a list the first call already had in hand. Measured +// on a Redmi 9A, that second call was 7.0 ms of a 23.5 ms lookup. typedef struct FFDexParcelRequest { const char* classDescriptor; + + // The write sequence. Left out by a request that only wants the instance fields, in which case + // `fields`, `capacity` and `count` are not read. const char* methodName; FFDexParcelField* fields; uint32_t capacity; uint32_t* count; // how many fields the sequence holds, or 0 when the jar cannot answer + + // The instance fields. 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. A declaration is a field set rather + // than an order, which is exactly what this answers. + const char* const* instanceFieldNames; + bool* instanceFieldResults; + uint32_t instanceFieldCount; } 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. +// Resolves 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. +// Every request is written its answers before anything can fail: a count of 0 for the sequence, and +// `false` for every instance field. A class the jar does not declare, a method it does not declare, +// and a sequence longer than the caller's `capacity` all leave the count at 0, 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 616e2ab56..2b3c3bf3d 100644 --- a/src/detection/displayserver/displayserver_android.c +++ b/src/detection/displayserver/displayserver_android.c @@ -411,6 +411,20 @@ static bool detectWithDumpsys(FFDisplayServerResult* ds) { #define FF_DISPLAY_ANDROID_NESTED_FIELDS 8 #define FF_DISPLAY_ANDROID_MODE_FIELDS 12 +// The two instance fields of `DisplayCutout` that lengthen its body, in the order the request writes +// their results. `mCutoutPathParserInfo` arrived in Android 13 and adds nine writes, `mSideOverrides` +// in 16 and adds one; Android 11 declares neither. +typedef enum FFDisplayAndroidCutoutField : uint32_t { + FF_DISPLAY_ANDROID_CUTOUT_PATH_PARSER_INFO, + FF_DISPLAY_ANDROID_CUTOUT_SIDE_OVERRIDES, + FF_DISPLAY_ANDROID_CUTOUT_FIELD_COUNT, +} FFDisplayAndroidCutoutField; + +static const char* const ffDisplayAndroidCutoutFields[FF_DISPLAY_ANDROID_CUTOUT_FIELD_COUNT] = { + "mCutoutPathParserInfo", + "mSideOverrides", +}; + // 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. @@ -432,13 +446,14 @@ typedef struct FFDisplayAndroidLayout { // `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; + // long its body is, is which fields the class declares, and that is what is asked for here. They + // are one array rather than two members because the request writes them as a run. + bool cutoutFields[FF_DISPLAY_ANDROID_CUTOUT_FIELD_COUNT]; } FFDisplayAndroidLayout; -// Reads every sequence above out of the dex entries of `jarPath`, in one lookup. +// Reads every sequence above, and `DisplayCutout`'s declared fields, out of the dex entries of +// `jarPath` -- in one lookup, because the walk behind it is what a lookup costs and the class data it +// reads along the way already holds everything asked for here. // // 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 @@ -499,6 +514,18 @@ static bool displayLayoutLoad(const char* jarPath, FFDisplayAndroidLayout* layou .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS, .count = &layout->frameRateCategoryRateCount, }, + { + // The one class whose body is not field-driven. `writeCutoutToParcel` reads everything + // through compiler-synthesised accessors, so there is no `iget` to walk and no class name + // on the wire either -- what says how long its body is, is which fields the class + // declares. Asking here rather than by a call of its own is the point: the walk that + // answers the seven sequences above is already holding this class's field list, and a + // second call would repeat it from the mapping down for 7.0 ms of a 23.5 ms lookup. + .classDescriptor = FF_DISPLAY_ANDROID_CLASS_CUTOUT, + .instanceFieldNames = ffDisplayAndroidCutoutFields, + .instanceFieldResults = layout->cutoutFields, + .instanceFieldCount = ARRAY_SIZE(ffDisplayAndroidCutoutFields), + }, }; const char* error = ffDexParcelFields(jarPath, requests, ARRAY_SIZE(requests)); @@ -512,28 +539,11 @@ static bool displayLayoutLoad(const char* jarPath, FFDisplayAndroidLayout* layou 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" + layout->cutoutFields[FF_DISPLAY_ANDROID_CUTOUT_PATH_PARSER_INFO] ? "yes" : "no", + layout->cutoutFields[FF_DISPLAY_ANDROID_CUTOUT_SIDE_OVERRIDES] ? "yes" : "no" ); return true; } @@ -1044,7 +1054,8 @@ static bool readerDisplayCutout(FFDisplayAndroidReader* reader, const FFDisplayA return false; } - if (layout->cutoutPathParserInfo) { + const bool* cutout = layout->cutoutFields; + if (cutout[FF_DISPLAY_ANDROID_CUTOUT_PATH_PARSER_INFO]) { // the display size the cutout was built for and then the physical one if (!readerSkip(reader, 4 * sizeof(int32_t))) { return false; @@ -1060,7 +1071,7 @@ static bool readerDisplayCutout(FFDisplayAndroidReader* reader, const FFDisplayA return false; } } - if (layout->cutoutSideOverrides) { + if (cutout[FF_DISPLAY_ANDROID_CUTOUT_SIDE_OVERRIDES]) { return readerSkipArray(reader, sizeof(int32_t)); // mSideOverrides } return true;