Common (Android): unifies dex parsers

This commit is contained in:
Carter Li
2026-10-10 23:55:38 +08:00
parent 7f9faeb720
commit d7b66c8677
3 changed files with 146 additions and 197 deletions
+76 -142
View File
@@ -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;
+33 -29
View File
@@ -122,42 +122,46 @@ typedef struct FFDexParcelField {
char type[FF_DEX_PARCEL_FIELD_TYPE_MAX];
} FFDexParcelField;
// One `<classDescriptor>.<methodName>` 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 `<classDescriptor>` declares an instance field named `<fieldName>`.
//
// 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);
@@ -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;