redex icon indicating copy to clipboard operation
redex copied to clipboard

R8 rejects DebugInfo

Open benjaminRomano opened this issue 4 years ago • 5 comments

Background In our build pipeline, D8 is ran after Redex on some of our DEX files which fails on parsing debug info.

Stacktraces

Stacktrace 1:

	[CIRCULAR REFERENCE:java.lang.AssertionError]
Caused by: java.lang.AssertionError
	at com.android.tools.r8.utils.LebUtils.parseUleb128(LebUtils.java:26)
	at com.android.tools.r8.dex.BinaryReader.getUleb128(BinaryReader.java:44)
	at com.android.tools.r8.dex.DexParser.parseDebugInfo(DexParser.java:501)
	at com.android.tools.r8.dex.DexParser.cacheAt(DexParser.java:418)
	at com.android.tools.r8.dex.DexParser.debugInfoAt(DexParser.java:497)
	at com.android.tools.r8.dex.DexParser.parseCodeItem(DexParser.java:897)
	at com.android.tools.r8.dex.DexParser.ensureCodesInited(DexParser.java:162)
	at com.android.tools.r8.dex.DexParser.readMethods(DexParser.java:642)
	at com.android.tools.r8.dex.DexParser.addClassDefsTo(DexParser.java:746)
	at com.android.tools.r8.dex.ApplicationReader$ClassReader.lambda$readDexSources$0(ApplicationReader.java:279)
	at java.util.concurrent.ForkJoinTask$AdaptedRunnableAction.exec(ForkJoinTask.java:1386)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

Stacktrace 2:

Caused by: java.lang.AssertionError
	at com.android.tools.r8.dex.JumboStringRewriter.recordDebugEventTargets(JumboStringRewriter.java:519)
	at com.android.tools.r8.dex.JumboStringRewriter.recordTargets(JumboStringRewriter.java:587)
	at com.android.tools.r8.dex.JumboStringRewriter.rewrite(JumboStringRewriter.java:113)
	at com.android.tools.r8.graph.DexEncodedMethod.rewriteCodeWithJumboStrings(DexEncodedMethod.java:1155)
	at com.android.tools.r8.dex.ApplicationWriter.lambda$rewriteCodeWithJumboStrings$3(ApplicationWriter.java:573)
	at com.android.tools.r8.graph.DexClass.forEachMethod(DexClass.java:315)
	at com.android.tools.r8.dex.ApplicationWriter.rewriteCodeWithJumboStrings(ApplicationWriter.java:570)
	at com.android.tools.r8.dex.ApplicationWriter.lambda$write$1(ApplicationWriter.java:293)
	at java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1424)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

I'm able to resolve the first stacktrace by cherrypicking the following commits related to DebugInfo into stable:

  • c811108ae68a3496b47817f50d96e4156315b9cd
  • 23daef8a861e5cb54de2a767db46015964d973b3
  • d55a36422cc5ec8c386db95df47f18dfb8fd7c07
  • 86a96e90f6ad95f4432034ced76a6e4e5b7cb63c
  • b9cd1537f738fd4239ecef9f2b9be804336e00b4

For the second stacktrace, I did not see any fixes related to JumboStrings. Here is the relevant info from r8 debugging:

// <PC>: <PC in hex>: <instruction>
0:   0x00: ConstClass          v2, java.io.IOException
2:   0x02: CheckCast           v5, com.google.android.gms.tasks.zzu
4:   0x04: IgetObject          v3, v5, Field java.lang.Object com.google.android.gms.tasks.zzu.mLock
6:   0x06: MonitorEnter        v3
7:   0x07: IgetBoolean         v1, v5, Field boolean com.google.android.gms.tasks.zzu.zzagf
9:   0x09: ConstString         v0, "Task is not yet complete"
11:   0x0b: InvokeStatic        { v1 v0 } Lcom/google/android/gms/ads/impl/R$string;->checkState(ZLjava/lang/Object;)V
14:   0x0e: IgetBoolean         v0, v5, Field boolean com.google.android.gms.tasks.zzu.zzfi
16:   0x10: IfNez               v0, 0x87 (+119)
18:   0x12: IgetObject          v0, v5, Field java.lang.Exception com.google.android.gms.tasks.zzu.zzagh
20:   0x14: InvokeVirtual       { v2 v0 } Ljava/lang/Class;->isInstance(Ljava/lang/Object;)Z
23:   0x17: MoveResult          v0
24:   0x18: IfNez               v0, 0x7e (+102)
26:   0x1a: IgetObject          v1, v5, Field java.lang.Exception com.google.android.gms.tasks.zzu.zzagh
28:   0x1c: IfNez               v1, 0x78 (+92)
30:   0x1e: IgetObject          v2, v5, Field java.lang.Object com.google.android.gms.tasks.zzu.zzagg
32:   0x20: MonitorExit         v3
33:   0x21: CheckCast           v2, android.os.Bundle
35:   0x23: ConstString         v3, "SERVICE_NOT_AVAILABLE"
37:   0x25: IfEqz               v2, 0x72 (+77)
39:   0x27: ConstStringJumbo    v0, "registration_id"
42:   0x2a: InvokeVirtual       { v2 v0 } Landroid/os/Bundle;->getString(Ljava/lang/String;)Ljava/lang/String;
45:   0x2d: MoveResultObject    v0
46:   0x2e: IfNez               v0, 0x57 (+41)
48:   0x30: ConstStringJumbo    v0, "unregistered"
51:   0x33: InvokeVirtual       { v2 v0 } Landroid/os/Bundle;->getString(Ljava/lang/String;)Ljava/lang/String;
54:   0x36: MoveResultObject    v0
55:   0x37: IfNez               v0, 0x57 (+32)
57:   0x39: ConstString         v0, "error"
59:   0x3b: InvokeVirtual       { v2 v0 } Landroid/os/Bundle;->getString(Ljava/lang/String;)Ljava/lang/String;
62:   0x3e: MoveResultObject    v1
63:   0x3f: ConstString         v0, "RST"
65:   0x41: InvokeVirtual       { v0 v1 } Ljava/lang/String;->equals(Ljava/lang/Object;)Z
68:   0x44: MoveResult          v0
69:   0x45: IfNez               v0, 0x4f (+10)
71:   0x47: IfEqz               v1, 0x58 (+17)
73:   0x49: NewInstance         v0, java.io.IOException
75:   0x4b: InvokeDirect        { v0 v1 } Ljava/io/IOException;-><init>(Ljava/lang/String;)V
78:   0x4e: Throw               v0
79:   0x4f: NewInstance         v1, java.io.IOException
81:   0x51: ConstString         v0, "INSTANCE_ID_RESET"
83:   0x53: InvokeDirect        { v1 v0 } Ljava/io/IOException;-><init>(Ljava/lang/String;)V
86:   0x56: Throw               v1
87:   0x57: ReturnObject        v0
88:   0x58: InvokeStatic        { v2 } Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String;
91:   0x5b: MoveResultObject    v2
92:   0x5c: InvokeVirtual       { v2 } Ljava/lang/String;->length()I
95:   0x5f: MoveResult          v0
96:   0x60: AddIntLit8          v1, v0, #21
98:   0x62: ConstString         v0, "Unexpected response: "
100:   0x64: InvokeStatic        { v1 v0 v2 } Lcom/android/tools/r8/GeneratedOutlineSupport;->outline141(ILjava/lang/String;Ljava/lang/String;)Ljava/lang/String;
103:   0x67: MoveResultObject    v2
104:   0x68: NewInstance         v1, java.lang.Throwable
106:   0x6a: InvokeDirect        { v1 } Ljava/lang/Throwable;-><init>()V
109:   0x6d: ConstString         v0, "FirebaseInstanceId"
111:   0x6f: InvokeStatic        { v0 v2 v1 } Landroid/util/Log;->w(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I
114:   0x72: NewInstance         v0, java.io.IOException
116:   0x74: InvokeDirect        { v0 v3 } Ljava/io/IOException;-><init>(Ljava/lang/String;)V
119:   0x77: Throw               v0
120:   0x78: NewInstance         v0, com.google.android.gms.tasks.RuntimeExecutionException
122:   0x7a: InvokeDirect        { v0 v1 } Lcom/google/android/gms/tasks/RuntimeExecutionException;-><init>(Ljava/lang/Throwable;)V
125:   0x7d: Throw               v0
126:   0x7e: IgetObject          v0, v5, Field java.lang.Exception com.google.android.gms.tasks.zzu.zzagh
128:   0x80: InvokeVirtual       { v2 v0 } Ljava/lang/Class;->cast(Ljava/lang/Object;)Ljava/lang/Object;
131:   0x83: MoveResultObject    v0
132:   0x84: CheckCast           v0, java.lang.Throwable
134:   0x86: Throw               v0
135:   0x87: NewInstance         v1, java.util.concurrent.CancellationException
137:   0x89: ConstString         v0, "Task is already canceled."
139:   0x8b: InvokeDirect        { v1 v0 } Ljava/util/concurrent/CancellationException;-><init>(Ljava/lang/String;)V
142:   0x8e: Throw               v1
143:   0x8f: MoveException       v0
144:   0x90: MonitorExit         v3
145:   0x91: Throw               v0

Here is some debugging info I collected using a breakpoint before the failing assert. Note, the pogram counter gets incremented to address 27 which does not have a corresponding instruction.

Program Counter increase for each Debug event from DebugInfo: Screenshot from 2020-08-04 15-09-47

Debug events from DebugInfo: Screenshot from 2020-08-04 15-10-15

Let me know if there is any other info I can provide.

cc @danzimm

benjaminRomano avatar Aug 04 '20 22:08 benjaminRomano

I'm able to resolve the first stacktrace by cherrypicking the following commits related to DebugInfo into stable: c811108ae68a3496b47817f50d96e4156315b9cd, 23daef8a861e5cb54de2a767db46015964d973b3, d55a36422cc5ec8c386db95df47f18dfb8fd7c07, 86a96e90f6ad95f4432034ced76a6e4e5b7cb63c, b9cd1537f738fd4239ecef9f2b9be804336e00b4

Which repo are these commit hashes from? I get a 404 when I try to use them in this repo

I'm curious which debug_info_kind you're using? It's specified in the redex config and here's a list of the options with some comments. Does switching to "no_custom_symbolication" workaround this issue?

(note: use this function to get the string names used in the config)

justinjhendrick avatar Aug 04 '20 23:08 justinjhendrick

I have the following defined in the config: "debug_info_kind": "no_custom_symbolication".

Sorrry I seem to have copied over the SHAs from cherrypicking and not the originals. Here are the correct ones: https://github.com/facebook/redex/commit/ca7fc6936f49fd682349227224989857e25d314e https://github.com/facebook/redex/commit/30d194f64dc197d82aabbf82d7037f5897a5eebf https://github.com/facebook/redex/commit/27ea91a5ed8c7b609291f62ae9f402e3eb37d631 https://github.com/facebook/redex/commit/117f3bed1f1183748ca41789b862234f85643dcf https://github.com/facebook/redex/commit/60e711b3a36977c830eb75f59f192ca589b12331

benjaminRomano avatar Aug 04 '20 23:08 benjaminRomano

Chatted a bit with Ben offline about the second stack trace. Sounds like the debug info being emitted is technically malformed but wouldn't cause any issue at runtime- I guess d8/r8 is a stickler for this kind of thing. Specifically certain debug info items are getting emitted with more address mappings that there are addresses in the code item pointing to it.

This must be an artifact from IODI (as IODI's whole point is to emit extra mappings for functions so that debug info can be reused), so I'll take a look within the next week or so and hopefully get a PR up to fix it!

danzimm avatar Aug 05 '20 22:08 danzimm

Ok, I think I a have a decent grasp on what's going on here. With Zim's help, we were able to create a python script to find all instances of the debug info referencing invalid PCs. I was also able to reproduce the issue when no passes were supplied.

In one of the instances, I observed that the positions in IR were correct but when lowering in generate_debug_instructions, converting positions to DebugInstructions would skip some ADVANCE_PC instructions. The reason being only a subset of positions included a file name and this is required to be converted into debug instructions. I'm not sure if this restriction is actually necessary.

It is expected that none of our positions have source files as we strip these out with R8. During Redex's deobfuscation step, it generates a best guess source file name if and only if the given position was remapped (i.e. different method/line number). Given the following mapping:

org.bouncycastle.crypto.engines.AESFastEngine -> org.bouncycastle.crypto.engines.AESFastEngine:
    1:1:void unpackBlock(byte[],int):0:0 -> processBlock
    1:1:int processBlock(byte[],int,byte[],int):0 -> processBlock
    2:2:int processBlock(byte[],int,byte[],int):0:0 -> processBlock
    3:3:void encryptBlock(int[][]):0:0 -> processBlock
    3:3:int processBlock(byte[],int,byte[],int):0 -> processBlock
    4:4:int processBlock(byte[],int,byte[],int):0:0 -> processBlock
    5:5:void decryptBlock(int[][]):0:0 -> processBlock
    5:5:int processBlock(byte[],int,byte[],int):0 -> processBlock
    6:6:void packBlock(byte[],int):0:0 -> processBlock
    6:6:int processBlock(byte[],int,byte[],int):0 -> processBlock
    7:7:int processBlock(byte[],int,byte[],int):0:0 -> processBlock

processBlock's debug info will be given a source file for lines 1, 3, 5 and 6 as there are multiple entries and the first corresponds to a different method in each instance. Note, if processBlock always came before other methods then no source file names would have been generated. So there could potentially be some non-deteriministic behavior here.

I suspect the original issue with debug instructions not being emitted for positions without file names is due to the following code block in generate_debug_instructions:

  for (auto it = entries.begin(); it != entries.end(); ++it) {
    // find all entries that belong to the same address, and group them by type
    auto addr = it->addr;
    std::vector<DexPosition*> positions;
    std::vector<DexDebugInstruction*> insns;
    for (; it != entries.end() && it->addr == addr; ++it) {
      switch (it->type) {
      case DexDebugEntryType::Position:
        if (it->pos->file != nullptr) {
          positions.push_back(it->pos.get());
        }
        break;
      case DexDebugEntryType::Instruction:
        insns.push_back(it->insn.get());
        break;
      }
    }
    --it;
    auto addr_delta = addr - prev_addr;
    prev_addr = addr;

    for (auto pos : positions) {
      pos_mapper->register_position(pos);
    }
    // only emit the last position entry for a given address
    if (!positions.empty()) {
      // ...
    }

    for (auto insn : insns) {
      // Potential issue:
      if (addr_delta != 0) {
        dbgops.emplace_back(
            new DexDebugInstruction(DBG_ADVANCE_PC, addr_delta));
        addr_delta = 0;
      }
      dbgops.emplace_back(insn->clone());
    }
  }
  return dbgops;
}

Moving the conditional if (addr_delta != 0) { ... } block out of the for-loop ensures that a change to the address register generates a new instruction.

With this change and the cherrypicks mentioned above, D8 no longer fails on parsing debug info. As for the ProguardMap issue, I don't have a sense for what the correct solution is and with the proposed fix this weird behavior wouldn't affect us anymore.

benjaminRomano avatar Aug 14 '20 21:08 benjaminRomano

Actually scratch the last part. It's better to instead remove the source file requirement. The motivation being that a given address may have multiple positions without source files and we'd want to use the logic to only take the last position entry for a given address.

Making the following adjustment also makes the instructions consistent with what R8 is producing. With that said, I'm still seeing a large increase in debug info size as tracked in https://github.com/facebook/redex/issues/523

benjaminRomano avatar Aug 14 '20 22:08 benjaminRomano