“Maybe bug 77342775” Exception Analysis
[TOC]
1. Background
1.1 Executive summary
The host is the release package
The plugin packages the code in the org.apache.http.legacy.jar jar package, but it’s all empty implementation classes. For example, HttpRequestBase.
package org.apache.http.message;
public abstract class AbstractHttpMessage implements HttpMessage {
protected HeaderGroup headergroup;
protected HttpParams params;
protected AbstractHttpMessage(HttpParams params) {
throw new RuntimeException("Stub!");
}
......
}
package org.apache.http.client.methods;
public abstract class HttpRequestBase extends AbstractHttpMessage implements HttpUriRequest, AbortableHttpRequest, Cloneable {
@Deprecated
public HttpRequestBase() {
throw new RuntimeException("Stub!");
}
@Deprecated
public void setConnectionRequest(ClientConnectionRequest connRequest) throws IOException {
throw new RuntimeException("Stub!");
}
......
}
The code in the plugin implements the BodyParamsEntity and HttpRequest classes (inherited from the empty implementation class in the org.apache.http.legacy.jar package).
package com.xosp.android.framework.http.client;
public class BodyParamsEntity extends AbstractHttpEntity {
.......
}
package com.xosp.android.framework.http.client;
public class HttpRequest extends HttpRequestBase implements HttpEntityEnclosingRequest {
private HttpEntity entity;
private HttpMethod method;
private URIBuilder uriBuilder;
private Charset uriCharset;
@Override
public HttpEntity getEntity() {
return this.entity;
}
@Override
public void setEntity(HttpEntity httpEntity) {
this.entity = httpEntity;
}
......
}
1.2 Abnormal information
1.2.1 Class ‘x.x.x’ does not implement interface ‘y.y.y’
Class 'com.xosp.android.framework.http.client.entity.BodyParamsEntity' does not implement interface 'java.util.concurrent.locks.Lock' in call to 'void java.util.concurrent.locks.Lock.lock()' (declaration of 'org.apache.http.client.methods.HttpRequestBase' appears in /system/framework/org.apache.http.legacy.boot.jar)
1.2.2 Maybe bug 77342775, looking for …
04-28 14:27:08.705 10893 11010 E com.xosp.example: Maybe bug 77342775, looking for Lorg/apache/http/HttpEntity; 0x137f34f0[continuous;main space (region space)] defined in /system/framework/org.apache.http.legacy.boot.jar/0x7884cd4e40
04-28 14:27:08.705 10893 11010 E com.xosp.example: with loader: dalvik.system.PathClassLoader/0x787c2f7400[hit:continuous;main space (region space)](/system/framework/org.apache.http.legacy.boot.jar/0x7884cd4e40:/data/app/com.xosp.example-eLNLa1Ch8IMzsPBMytsZrw==/base.apk/0x7884df81c0:+!classes2.dex/0x7884df8540:+!classes3.dex/0x7884df87e0);java.lang.BootClassLoader/0x7884cbb780
04-28 14:27:08.705 10893 11010 E com.xosp.example: in interface table for Ljava/util/concurrent/locks/ReentrantLock; 0x6f7af018[image;/data/dalvik-cache/arm64/system@framework@boot-core-oj.art;+;0x6f713000] defined in /system/framework/core-oj.jar/0x7884cd0140 ifcount=2
04-28 14:27:08.706 10893 11010 E com.xosp.example: with loader BootClassLoader
04-28 14:27:08.706 10893 11010 E com.xosp.example: iface #0: java.util.concurrent.locks.Lock
04-28 14:27:08.706 10893 11010 E com.xosp.example: iface #1: java.io.Serializable
1.3 HttpRequestBase (system implementation class)
package org.apache.http.client.methods;
public abstract class AbstractHttpMessage implements HttpMessage {
protected HeaderGroup headergroup;
protected HttpParams params;
protected AbstractHttpMessage(HttpParams params) {
this.headergroup = new HeaderGroup();
this.params = params;
}
......
}
package org.apache.http.client.methods;
public abstract class HttpRequestBase extends AbstractHttpMessage implements HttpUriRequest, AbortableHttpRequest, Cloneable {
private Lock abortLock = new ReentrantLock();
private boolean aborted;
private ClientConnectionRequest connRequest;
private ConnectionReleaseTrigger releaseTrigger;
private URI uri;
@Override
public void setConnectionRequest(ClientConnectionRequest connRequest) throws IOException {
this.abortLock.lock();
try {
if (this.aborted) {
throw new IOException("Request already aborted");
}
this.releaseTrigger = null;
this.connRequest = connRequest;
} finally {
this.abortLock.unlock();
}
}
......
}
1.4 On inlining
Probably the first thing many people will think of when they see this issue is the inline problem with pluginization on Android 9. But if it is caused by inlining, then since the classes in /system/framework/org.apache.http.legacy.boot.jar are on a separate Dex, it will first report Inlined method resolution crossed dex file boundary exception, but we analyzed all the logs (including network related issues) and found nothing. boundary exception, but we analyzed all the logs (including network related issues) and found no relevant information.
Also if the problem is inline, there should be crash scenarios caused by native exceptions like Fatal signal xx, but in our logs on various Android 9 devices it’s IncompatibleClassChangeError, and we didn’t find any cases of native crashes:
java.lang.IncompatibleClassChangeError: Class 'com.xosp.android.framework.http.client.entity.BodyParamsEntity' does not implement interface 'java.util.concurrent.locks.Lock' in call to 'void java.util.concurrent.locks.Lock.lock()' (declaration of 'org.apache.http.client.methods.HttpRequestBase' appears in /system/framework/org.apache.http.legacy.boot.jar)
So we started to wonder if there was something wrong with the dex2oat compiled and optimized code, and sure enough.
2. dex2oat compilation mode
2.1 Four compilation modes
verify: only run DEX code verification (no AOT compilation).
quicken: (until Android 11) run DEX code verification and optimize some DEX instructions to get better interpreter performance.
speed: Run DEX code verification and AOT-compile all methods.
speed-profile: Run DEX code verification and AOT-compile methods listed in a profile file.
For quicken mode, in addition to running DEX code verification, it will also optimize some DEX commands, so what exactly is optimized? Let’s manually trigger different modes of dex2oat in the emulator to have a brief comparison.
2.1.1 verify
- adb shell cmd package compile -m verify -f com.xosp.example
11: void com.xosp.android.framework.http.client.HttpRequest.setEntity(org.apache.http.HttpEntity) (dex_method_idx=34256)
DEX CODE:
0x0000: 5b01 7041 | iput-object v1, v0, Lorg/apache/http/HttpEntity; com.xosp.android.framework.http.client.HttpRequest.entity // field@16752
0x0002: 7300 | return-void-no-barrier
OatMethodOffsets (offset=0x00000000)
code_offset: 0x00000000
OatQuickMethodHeader (offset=0x00000000)
vmap_table: (offset=0x00000000)
QuickMethodFrameInfo
frame_size_in_bytes: 0
core_spill_mask: 0x00000000
fp_spill_mask: 0x00000000
vr_stack_locations:
ins: v0[sp + #8] v1[sp + #12]
method*: v2[sp + #0]
CODE: (code_offset=0x00000000 size_offset=0x00000000 size=0)
NO CODE!
From the output of oatdump, we can see that for verify mode, DEX’s commands are not optimized, and are still the basic iput-object v1, v0
Opcode (hex) | Opcode name | Explanation | Example |
---|---|---|---|
5B | iput-object vx,vy,field_id | Puts the object reference in vx into an instance field. The instance is referenced by vy. | 5B20 0000 – iput-object v0, v2, LineReader.bis:Ljava/io/BufferedInputStream; // field@0000 Stores the object reference in v0 into field@0000 (entry #0 in the field table). The instance is referenced by v2. |
2.2.1 quicken
- adb shell cmd package compile -m quicken -f com.xosp.example
11: void com.xosp.android.framework.http.client.HttpRequest.setEntity(org.apache.http.HttpEntity) (dex_method_idx=34256)
DEX CODE:
0x0000: e801 2400 | iput-object-quick v1, v0, // offset@36
0x0002: 7300 | return-void-no-barrier
OatMethodOffsets (offset=0x00000000)
code_offset: 0x00000000
OatQuickMethodHeader (offset=0x00000000)
vmap_table: (offset=0x00000000)
QuickMethodFrameInfo
frame_size_in_bytes: 0
core_spill_mask: 0x00000000
fp_spill_mask: 0x00000000
vr_stack_locations:
ins: v0[sp + #8] v1[sp + #12]
method*: v2[sp + #0]
CODE: (code_offset=0x00000000 size_offset=0x00000000 size=0)
NO CODE!
And for quicken mode, we can see that the directive has been replaced with iput-object-quick v1, v0, // offset@36.
The iput-object-quick instruction implements the variable saving logic by directly modifying the memory through the start address and offset value of the object data area.
Opcode (hex) | Opcode name | Explanation | Example |
---|---|---|---|
F7 | iput-object-quick vx,vy,offset | Puts the object reference value stored in vx to offset in vy instance’s data area to vx6. | F701 4C00 – iput-object-quick v1, v0, [obj+004c] Puts the object reference value in v1 to offset 0CH of the instance pointed by v3. |
2.2.2 speed
- adb shell cmd package compile -m speed -f com.xosp.example
11: void com.xosp.android.framework.http.client.HttpRequest.setEntity(org.apache.http.HttpEntity) (dex_method_idx=34256)
DEX CODE:
0x0000: 5b01 7041 | iput-object v1, v0, Lorg/apache/http/HttpEntity; com.xosp.android.framework.http.client.HttpRequest.entity // field@16752
0x0002: 7300 | return-void-no-barrier
OatMethodOffsets (offset=0x0003c7c0)
code_offset: 0x004abbd0
OatQuickMethodHeader (offset=0x004abbb8)
vmap_table: (offset=0x0044d5b4)
Optimized CodeInfo (number_of_dex_registers=2, number_of_stack_maps=0)
StackMapEncoding (native_pc_bit_offset=0, dex_pc_bit_offset=0, dex_register_map_bit_offset=0, inline_info_bit_offset=0, register_mask_bit_offset=0, stack_mask_index_bit_offset=0, total_bit_size=0)
DexRegisterLocationCatalog (number_of_entries=0, size_in_bytes=0)
QuickMethodFrameInfo
frame_size_in_bytes: 0
core_spill_mask: 0x40000000 (r30)
fp_spill_mask: 0x00000000
vr_stack_locations:
ins: v0[sp + #8] v1[sp + #12]
method*: v2[sp + #0]
CODE: (code_offset=0x004abbd0 size_offset=0x004abbcc size=24)...
0x004abbd0: b9002422 str w2, [x1, #36]
0x004abbd4: 34000082 cbz w2, #+0x10 (addr 0x4abbe4)
0x004abbd8: f9404670 ldr x16, [tr, #136] ; card_table
0x004abbdc: 530a7c31 lsr w17, w1, #10
0x004abbe0: 38316a10 strb w16, [x16, x17]
0x004abbe4: d65f03c0 ret
For speed mode, we can see that the CODE already has compiled machine code. We won’t go into the details here, if you are interested, you can analyze it by yourself.
3. Analysis of the problem
From the compilation mode of dex2oat above, we can see that quicken mode optimizes the DEX instruction, while verify mode does not. Coincidentally, the default compilation mode on Android 9 is quicken, so we decided to dump the plugin’s odex code for comparison.
3.1 Android 8.0 plugin loading
classpath = &
compiler-filter = quicken
concurrent-copying = true
debuggable = false
dex2oat-cmdline = --instruction-set=arm64 --instruction-set-features=a53 --runtime-arg -Xrelocate --boot-image=/system/framework/boot.art --runtime-arg -Xms64m --runtime-arg -Xmx512m --instruction-set-variant=generic --instruction-set-features=default --dex-file=/data/data/com.xosp.example/files/plugin/0/base.apk --output-vdex-fd=196 --oat-fd=197 --oat-location=/data/data/com.xosp.example/files/plugin/0/oat/arm64/base.odex --compiler-filter=quicken --class-loader-context=&
dex2oat-host = Arm64
image-location = /data/dalvik-cache/arm64/system@framework@boot.art:/data/dalvik-cache/arm64/system@framework@boot-core-libart.art:/data/dalvik-cache/arm64/system@framework@boot-conscrypt.art:/data/dalvik-cache/arm64/system@framework@boot-okhttp.art:/data/dalvik-cache/arm64/system@framework@boot-bouncycastle.art:/data/dalvik-cache/arm64/system@framework@boot-apache-xml.art:/data/dalvik-cache/arm64/system@framework@boot-legacy-test.art:/data/dalvik-cache/arm64/system@framework@boot-ext.art:/data/dalvik-cache/arm64/system@framework@boot-framework.art:/data/dalvik-cache/arm64/system@framework@boot-telephony-common.art:/data/dalvik-cache/arm64/system@framework@boot-voip-common.art:/data/dalvik-cache/arm64/system@framework@boot-ims-common.art:/data/dalvik-cache/arm64/
system@framework@boot-org.apache.http.legacy.boot.art:/data/dalvik-cache/arm64/system@framework@boot-android.hidl.base-V1.0-java.art:/data/dalvik-cache/arm64/system@framework@boot-android.hidl.manager-V1.0-java.art
native-debuggable = false
pic = false
11: void com.xosp.android.framework.http.client.HttpRequest.setEntity(org.apache.http.HttpEntity) (dex_method_idx=34256)
DEX CODE:
0x0000: e801 2400 | iput-object-quick v1, v0, // offset@36
0x0002: 7300 | return-void-no-barrier
......
CODE: (code_offset=0x00000000 size_offset=0x00000000 size=0)
NO CODE!
8: org.apache.http.HttpEntity com.xosp.android.framework.http.client.HttpRequest.getEntity() (dex_method_idx=34252)
DEX CODE:
0x0000: e510 2400 | iget-object-quick v0, v1, // offset@36
0x0002: 1100 | return-object v0
......
CODE: (code_offset=0x00000000 size_offset=0x00000000 size=0)
NO CODE!
When we load it as a plugin on an Android 8 machine, we can see that after compiling through dex2oat, the setEntity method calls the instruction iput-object-quick v1, v0, // offset@36, paying special attention to the fact that the offset here is 36.
3.2 Android 9.0 plugin loading
classpath = &
compiler-filter = quicken
concurrent-copying = true
debuggable = false
dex2oat-cmdline = /system/bin/dex2oat --instruction-set=arm64 --instruction-set-features=a53 --runtime-arg -Xhidden-api-checks --runtime-arg -Xrelocate --boot-image=/system/framework/boot.art --runtime-arg -Xms64m --runtime-arg -Xmx512m --instruction-set-variant=generic --instruction-set-features=default --dex-file=/./data/user/0/com.xosp.example/files/plugin/0/base.apk --output-vdex-fd=146 --oat-fd=152 --oat-location=/./data/user/0/com.xosp.example/files/plugin/0/oat/arm64/base.odex --compiler-filter=quicken --class-loader-context=&
dex2oat-host = Arm64
image-location = /data/dalvik-cache/arm64/system@framework@boot.art:/data/dalvik-cache/arm64/system@framework@boot-core-libart.art:/data/dalvik-cache/arm64/system@framework@boot-conscrypt.art:/data/dalvik-cache/arm64/system@framework@boot-okhttp.art:/data/dalvik-cache/arm64/system@framework@boot-bouncycastle.art:/data/dalvik-cache/arm64/system@framework@boot-apache-xml.art:/data/dalvik-cache/arm64/system@framework@boot-ext.art:/data/dalvik-cache/arm64/system@framework@boot-framework.art:/data/dalvik-cache/arm64/system@framework@boot-telephony-common.art:/data/dalvik-cache/arm64/system@framework@boot-voip-common.art:/data/dalvik-cache/arm64/system@framework@boot-ims-common.art:/data/dalvik-cache/arm64/system@framework@boot-android.hidl.base-V1.0-java.art:/data/dalvik-cache/arm64/system@framework@boot-android.hidl.manager-V1.0-java.art:/data/dalvik-cache/arm64/system@framework@boot-framework-oahl-backward-compatibility.art:/data/dalvik-cache/arm64/system@framework@boot-android.test.base.art
native-debuggable = false
pic = false
11: void com.xosp.android.framework.http.client.HttpRequest.setEntity(org.apache.http.HttpEntity) (dex_method_idx=34256)
DEX CODE:
0x0000: e801 1000 | iput-object-quick v1, v0, // offset@16
0x0002: 7300 | return-void-no-barrier
......
CODE: (code_offset=0x00000000 size_offset=0x00000000 size=0)
NO CODE!
8: org.apache.http.HttpEntity com.xosp.android.framework.http.client.HttpRequest.getEntity() (dex_method_idx=34252)
DEX CODE:
0x0000: e510 1000 | iget-object-quick v0, v1, // offset@16
0x0002: 1100 | return-object v0
......
CODE: (code_offset=0x00000000 size_offset=0x00000000 size=0)
NO CODE!
However, when we load it as a plugin on an Android 9 machine, we see that after compiling via dex2oat, the setEntity method calls iput-object-quick v1, v0, // offset@16, notice how the offset becomes 16 here.
Notice that the difference between the address offsets 16 and 36 is 20, which is exactly the same as the reference to the last 5 objects, see HttpRequestBase (system implementation class) above.
3.3 Android 9.0 Direct Installation
!!!这里 classpath 把 org.apache.http.legacy.boot.jar
classpath = PCL[/system/framework/org.apache.http.legacy.boot.jar*796383208]
compilation-reason = install
compiler-filter = speed-profile
concurrent-copying = true
debuggable = false
dex2oat-cmdline = /system/bin/dex2oat --zip-fd=6 --zip-location=base.apk --input-vdex-fd=-1 --output-vdex-fd=8 --oat-fd=7 --oat-location=/data/app/com.xosp.example/oat/arm64/base.odex --instruction-set=arm64 --instruction-set-variant=generic --instruction-set-features=default --runtime-arg -Xms64m --runtime-arg -Xmx512m --compiler-filter=speed-profile --swap-fd=9 --app-image-fd=10 --image-format=lz4 --classpath-dir=/data/app/com.xosp.example-uVSqKqKftrQRujUYmb4Qwg== --class-loader-context=PCL[/system/framework/org.apache.http.legacy.boot.jar] --generate-mini-debug-info --compact-dex-level=none --runtime-arg -Xtarget-sdk-version:30 --runtime-arg -Xhidden-api-checks --compilation-reason=install
dex2oat-host = Arm64
image-location = /data/dalvik-cache/arm64/system@framework@boot.art:/data/dalvik-cache/arm64/system@framework@boot-core-libart.art:/data/dalvik-cache/arm64/system@framework@boot-conscrypt.art:/data/dalvik-cache/arm64/system@framework@boot-okhttp.art:/data/dalvik-cache/arm64/system@framework@boot-bouncycastle.art:/data/dalvik-cache/arm64/system@framework@boot-apache-xml.art:/data/dalvik-cache/arm64/system@framework@boot-ext.art:/data/dalvik-cache/arm64/system@framework@boot-framework.art:/data/dalvik-cache/arm64/system@framework@boot-telephony-common.art:/data/dalvik-cache/arm64/system@framework@boot-voip-common.art:/data/dalvik-cache/arm64/system@framework@boot-ims-common.art:/data/dalvik-cache/arm64/system@framework@boot-android.hidl.base-V1.0-java.art:/data/dalvik-cache/arm64/system@framework@boot-android.hidl.manager-V1.0-java.art:/data/dalvik-cache/arm64/system@framework@boot-framework-oahl-backward-compatibility.art:/data/dalvik-cache/arm64/system@framework@boot-android.test.base.art
native-debuggable = false
pic = false
11: void com.xosp.android.framework.http.client.HttpRequest.setEntity(org.apache.http.HttpEntity) (dex_method_idx=34256)
DEX CODE:
0x0000: e801 2400 | iput-object-quick v1, v0, // offset@36
0x0002: 7300 | return-void-no-barrier
......
CODE: (code_offset=0x00000000 size_offset=0x00000000 size=0)
NO CODE!
8: org.apache.http.HttpEntity com.xosp.android.framework.http.client.HttpRequest.getEntity() (dex_method_idx=34252)
DEX CODE:
0x0000: e510 2400 | iget-object-quick v0, v1, // offset@36
0x0002: 1100 | return-object v0
......
CODE: (code_offset=0x00000000 size_offset=0x00000000 size=0)
NO CODE!
Also, when we install it directly on an Android 9 machine, we see that after compiling through dex2oat, the setEntity method calls iput-object-quick v1, v0, // offset@36, where the offset is 36, which is the same as the code after optimization on Android 8.
4. Summary
Through the above analysis, we can see that the cause of this problem is the same as the exception thrown: IncompatibleClassChangeError, which simply means that the compile-time code is inconsistent with the run-time code.
The reason why the package org.apache.http.legacy.jar is often out of the picture is that when the plugin is packaged, it packages the empty implementation of the class; while in the actual runtime, it loads the class provided by the system. The two are not consistent in the structure of the class, thus leading to run-time memory or variable references are misaligned.
Theoretically, because of the quicken compilation optimization, the offset value of the address depends on the difference between the compile-time class and the run-time class, so it is also possible that other exception messages may be triggered, not just IncompatibleClassChangeError.
It’s actually quite easy to construct this scenario in a pluginized scenario.
5. More
5.1 Calculation Logic for FieldOffset
bool ClassLinker::LinkFields(Thread* self,
Handle<mirror::Class> klass,
bool is_static,
size_t* class_size) {
self->AllowThreadSuspension();
const size_t num_fields = is_static ? klass->NumStaticFields() : klass->NumInstanceFields();
LengthPrefixedArray<ArtField>* const fields = is_static ? klass->GetSFieldsPtr() :
klass->GetIFieldsPtr();
// Initialize field_offset
MemberOffset field_offset(0);
if (is_static) {
field_offset = klass->GetFirstReferenceStaticFieldOffsetDuringLinking(image_pointer_size_);
} else {
ObjPtr<mirror::Class> super_class = klass->GetSuperClass();
if (super_class != nullptr) {
CHECK(super_class->IsResolved())
<< klass->PrettyClass() << " " << super_class->PrettyClass();
field_offset = MemberOffset(super_class->GetObjectSize());
}
}
CHECK_EQ(num_fields == 0, fields == nullptr) << klass->PrettyClass();
// we want a relatively stable order so that adding new fields
// minimizes disruption of C++ version such as Class and Method.
//
// The overall sort order order is:
// 1) All object reference fields, sorted alphabetically.
// 2) All java long (64-bit) integer fields, sorted alphabetically.
// 3) All java double (64-bit) floating point fields, sorted alphabetically.
// 4) All java int (32-bit) integer fields, sorted alphabetically.
// 5) All java float (32-bit) floating point fields, sorted alphabetically.
// 6) All java char (16-bit) integer fields, sorted alphabetically.
// 7) All java short (16-bit) integer fields, sorted alphabetically.
// 8) All java boolean (8-bit) integer fields, sorted alphabetically.
// 9) All java byte (8-bit) integer fields, sorted alphabetically.
//
// Once the fields are sorted in this order we will attempt to fill any gaps that might be present
// in the memory layout of the structure. See ShuffleForward for how this is done.
......
......
......
}