Skip to content

Commit 55a7f6f

Browse files
authored
fix(crashlytics,android): fix an issue with deobfuscating flavored builds (#18085)
* fix(crashlytics,android): fix an issue with deobfuscating flavored builds * format * improve ELFBuildIdReader so it handles AAB as well * format
1 parent 1a20154 commit 55a7f6f

2 files changed

Lines changed: 414 additions & 3 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.firebase.crashlytics;
6+
7+
import android.content.Context;
8+
import android.util.Log;
9+
import java.io.File;
10+
import java.io.InputStream;
11+
import java.io.RandomAccessFile;
12+
import java.nio.ByteBuffer;
13+
import java.nio.ByteOrder;
14+
import java.util.Enumeration;
15+
import java.util.zip.ZipEntry;
16+
import java.util.zip.ZipFile;
17+
18+
/**
19+
* Reads the ELF build ID from libapp.so at runtime.
20+
*
21+
* <p>The Firebase CLI's {@code crashlytics:symbols:upload} command uses the ELF build ID (from the
22+
* {@code .note.gnu.build-id} section) when uploading symbols. To ensure Crashlytics can match crash
23+
* reports to uploaded symbols, the plugin must report the same ELF build ID rather than the Dart
24+
* VM's internal snapshot build ID (which may differ, especially for AAB + flavor builds).
25+
*/
26+
final class ElfBuildIdReader {
27+
28+
private static final String TAG = "FLTFirebaseCrashlytics";
29+
30+
private static final byte[] ELF_MAGIC = {0x7f, 'E', 'L', 'F'};
31+
private static final int ELFCLASS64 = 2;
32+
private static final int PT_NOTE = 4;
33+
private static final int NT_GNU_BUILD_ID = 3;
34+
private static final String GNU_NOTE_NAME = "GNU";
35+
36+
private ElfBuildIdReader() {}
37+
38+
/**
39+
* Reads the ELF build ID from libapp.so.
40+
*
41+
* <p>First checks the native library directory (for devices that extract native libs). If not
42+
* found there, reads libapp.so from inside the APK (for devices with extractNativeLibs=false).
43+
*
44+
* @return the build ID as a lowercase hex string, or {@code null} if it cannot be read.
45+
*/
46+
static String readBuildId(Context context) {
47+
try {
48+
// Try extracted native library first.
49+
String nativeLibDir = context.getApplicationInfo().nativeLibraryDir;
50+
File libApp = new File(nativeLibDir, "libapp.so");
51+
if (libApp.exists()) {
52+
return readBuildIdFromElf(libApp);
53+
}
54+
55+
// Fall back to reading from inside the APK (or split APKs for AAB installs).
56+
return readBuildIdFromApk(context);
57+
} catch (Exception e) {
58+
Log.d(TAG, "Could not read ELF build ID from libapp.so", e);
59+
return null;
60+
}
61+
}
62+
63+
/**
64+
* Reads the ELF build ID from libapp.so stored inside the APK. On newer Android versions, native
65+
* libraries may not be extracted to the filesystem.
66+
*/
67+
private static String readBuildIdFromApk(Context context) throws Exception {
68+
// Check the base APK first.
69+
String result = readBuildIdFromZip(context.getApplicationInfo().sourceDir);
70+
if (result != null) {
71+
return result;
72+
}
73+
74+
// For AAB installs, libapp.so is in a split APK (e.g., split_config.arm64_v8a.apk).
75+
String[] splitDirs = context.getApplicationInfo().splitSourceDirs;
76+
if (splitDirs != null) {
77+
for (String splitDir : splitDirs) {
78+
result = readBuildIdFromZip(splitDir);
79+
if (result != null) {
80+
return result;
81+
}
82+
}
83+
}
84+
return null;
85+
}
86+
87+
private static String readBuildIdFromZip(String apkPath) throws Exception {
88+
try (ZipFile zipFile = new ZipFile(apkPath)) {
89+
Enumeration<? extends ZipEntry> entries = zipFile.entries();
90+
while (entries.hasMoreElements()) {
91+
ZipEntry entry = entries.nextElement();
92+
if (entry.getName().endsWith("/libapp.so")) {
93+
try (InputStream is = zipFile.getInputStream(entry)) {
94+
byte[] elfData = new byte[(int) entry.getSize()];
95+
int offset = 0;
96+
while (offset < elfData.length) {
97+
int read = is.read(elfData, offset, elfData.length - offset);
98+
if (read < 0) break;
99+
offset += read;
100+
}
101+
return readBuildIdFromBytes(elfData);
102+
}
103+
}
104+
}
105+
}
106+
return null;
107+
}
108+
109+
private static String readBuildIdFromElf(File elfFile) throws Exception {
110+
try (RandomAccessFile raf = new RandomAccessFile(elfFile, "r")) {
111+
return readBuildIdFromRaf(raf);
112+
}
113+
}
114+
115+
private static String readBuildIdFromBytes(byte[] data) {
116+
try {
117+
ByteBuffer buf = ByteBuffer.wrap(data);
118+
119+
// Verify ELF magic bytes.
120+
for (int i = 0; i < 4; i++) {
121+
if (buf.get() != ELF_MAGIC[i]) {
122+
return null;
123+
}
124+
}
125+
126+
int elfClass = buf.get() & 0xFF; // 1 = 32-bit, 2 = 64-bit
127+
boolean is64 = elfClass == ELFCLASS64;
128+
129+
int dataEncoding = buf.get() & 0xFF; // 1 = little-endian, 2 = big-endian
130+
ByteOrder order = dataEncoding == 1 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;
131+
buf.order(order);
132+
133+
if (is64) {
134+
return readBuildIdFromBuffer64(buf);
135+
} else {
136+
return readBuildIdFromBuffer32(buf);
137+
}
138+
} catch (Exception e) {
139+
Log.d(TAG, "Could not parse ELF from APK", e);
140+
return null;
141+
}
142+
}
143+
144+
private static String readBuildIdFromBuffer64(ByteBuffer buf) {
145+
// e_phoff is at offset 32 in the 64-bit ELF header.
146+
buf.position(32);
147+
long phoff = buf.getLong();
148+
149+
// e_phentsize is at offset 54, e_phnum at offset 56.
150+
buf.position(54);
151+
int phentsize = buf.getShort() & 0xFFFF;
152+
int phnum = buf.getShort() & 0xFFFF;
153+
154+
for (int i = 0; i < phnum; i++) {
155+
int phdr = (int) (phoff + (long) i * phentsize);
156+
buf.position(phdr);
157+
int type = buf.getInt();
158+
if (type == PT_NOTE) {
159+
// p_offset is at phdr + 8, p_filesz at phdr + 32 for 64-bit.
160+
buf.position(phdr + 8);
161+
long noteOffset = buf.getLong();
162+
buf.position(phdr + 32);
163+
long noteSize = buf.getLong();
164+
165+
String buildId = findGnuBuildIdInBuffer(buf, noteOffset, noteSize);
166+
if (buildId != null) {
167+
return buildId;
168+
}
169+
}
170+
}
171+
return null;
172+
}
173+
174+
private static String readBuildIdFromBuffer32(ByteBuffer buf) {
175+
// e_phoff is at offset 28 in the 32-bit ELF header.
176+
buf.position(28);
177+
long phoff = buf.getInt() & 0xFFFFFFFFL;
178+
179+
// e_phentsize is at offset 42, e_phnum at offset 44.
180+
buf.position(42);
181+
int phentsize = buf.getShort() & 0xFFFF;
182+
int phnum = buf.getShort() & 0xFFFF;
183+
184+
for (int i = 0; i < phnum; i++) {
185+
int phdr = (int) (phoff + (long) i * phentsize);
186+
buf.position(phdr);
187+
int type = buf.getInt();
188+
if (type == PT_NOTE) {
189+
// p_offset is at phdr + 4, p_filesz at phdr + 16 for 32-bit.
190+
buf.position(phdr + 4);
191+
long noteOffset = buf.getInt() & 0xFFFFFFFFL;
192+
buf.position(phdr + 16);
193+
long noteSize = buf.getInt() & 0xFFFFFFFFL;
194+
195+
String buildId = findGnuBuildIdInBuffer(buf, noteOffset, noteSize);
196+
if (buildId != null) {
197+
return buildId;
198+
}
199+
}
200+
}
201+
return null;
202+
}
203+
204+
private static String findGnuBuildIdInBuffer(ByteBuffer buf, long offset, long size) {
205+
long end = offset + size;
206+
long pos = offset;
207+
208+
while (pos + 12 <= end) {
209+
buf.position((int) pos);
210+
int namesz = buf.getInt();
211+
int descsz = buf.getInt();
212+
int type = buf.getInt();
213+
214+
if (namesz < 0 || descsz < 0 || namesz > 256) {
215+
break;
216+
}
217+
218+
int nameAligned = align4(namesz);
219+
long descPos = pos + 12 + nameAligned;
220+
221+
if (namesz > 0 && type == NT_GNU_BUILD_ID && descPos + descsz <= end) {
222+
byte[] nameBytes = new byte[namesz];
223+
buf.get(nameBytes);
224+
String name =
225+
new String(
226+
nameBytes, 0, Math.max(0, namesz - 1), java.nio.charset.StandardCharsets.US_ASCII);
227+
228+
if (GNU_NOTE_NAME.equals(name) && descsz > 0) {
229+
buf.position((int) descPos);
230+
byte[] desc = new byte[descsz];
231+
buf.get(desc);
232+
return bytesToHex(desc);
233+
}
234+
}
235+
236+
pos = descPos + align4(descsz);
237+
}
238+
return null;
239+
}
240+
241+
private static String readBuildIdFromRaf(RandomAccessFile raf) throws Exception {
242+
// Verify ELF magic bytes.
243+
byte[] magic = new byte[4];
244+
raf.readFully(magic);
245+
for (int i = 0; i < 4; i++) {
246+
if (magic[i] != ELF_MAGIC[i]) {
247+
return null;
248+
}
249+
}
250+
251+
int elfClass = raf.read(); // 1 = 32-bit, 2 = 64-bit
252+
boolean is64 = elfClass == ELFCLASS64;
253+
254+
int dataEncoding = raf.read(); // 1 = little-endian, 2 = big-endian
255+
ByteOrder order = dataEncoding == 1 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;
256+
257+
if (is64) {
258+
return readBuildIdFromElf64(raf, order);
259+
} else {
260+
return readBuildIdFromElf32(raf, order);
261+
}
262+
}
263+
264+
private static String readBuildIdFromElf64(RandomAccessFile raf, ByteOrder order)
265+
throws Exception {
266+
// e_phoff is at offset 32 in the 64-bit ELF header.
267+
raf.seek(32);
268+
long phoff = readLong(raf, order);
269+
270+
// e_phentsize is at offset 54, e_phnum at offset 56.
271+
raf.seek(54);
272+
int phentsize = readUnsignedShort(raf, order);
273+
int phnum = readUnsignedShort(raf, order);
274+
275+
for (int i = 0; i < phnum; i++) {
276+
long phdr = phoff + (long) i * phentsize;
277+
raf.seek(phdr);
278+
int type = readInt(raf, order);
279+
if (type == PT_NOTE) {
280+
// p_offset is at phdr + 8, p_filesz at phdr + 32 for 64-bit.
281+
raf.seek(phdr + 8);
282+
long noteOffset = readLong(raf, order);
283+
raf.seek(phdr + 32);
284+
long noteSize = readLong(raf, order);
285+
286+
String buildId = findGnuBuildId(raf, noteOffset, noteSize, order);
287+
if (buildId != null) {
288+
return buildId;
289+
}
290+
}
291+
}
292+
return null;
293+
}
294+
295+
private static String readBuildIdFromElf32(RandomAccessFile raf, ByteOrder order)
296+
throws Exception {
297+
// e_phoff is at offset 28 in the 32-bit ELF header.
298+
raf.seek(28);
299+
long phoff = readInt(raf, order) & 0xFFFFFFFFL;
300+
301+
// e_phentsize is at offset 42, e_phnum at offset 44.
302+
raf.seek(42);
303+
int phentsize = readUnsignedShort(raf, order);
304+
int phnum = readUnsignedShort(raf, order);
305+
306+
for (int i = 0; i < phnum; i++) {
307+
long phdr = phoff + (long) i * phentsize;
308+
raf.seek(phdr);
309+
int type = readInt(raf, order);
310+
if (type == PT_NOTE) {
311+
// p_offset is at phdr + 4, p_filesz at phdr + 16 for 32-bit.
312+
raf.seek(phdr + 4);
313+
long noteOffset = readInt(raf, order) & 0xFFFFFFFFL;
314+
raf.seek(phdr + 16);
315+
long noteSize = readInt(raf, order) & 0xFFFFFFFFL;
316+
317+
String buildId = findGnuBuildId(raf, noteOffset, noteSize, order);
318+
if (buildId != null) {
319+
return buildId;
320+
}
321+
}
322+
}
323+
return null;
324+
}
325+
326+
/**
327+
* Searches a PT_NOTE segment for the GNU build ID note.
328+
*
329+
* <p>Note format: namesz (4) | descsz (4) | type (4) | name (aligned to 4) | desc (aligned to 4)
330+
*/
331+
private static String findGnuBuildId(
332+
RandomAccessFile raf, long offset, long size, ByteOrder order) throws Exception {
333+
long end = offset + size;
334+
long pos = offset;
335+
336+
while (pos + 12 <= end) {
337+
raf.seek(pos);
338+
int namesz = readInt(raf, order);
339+
int descsz = readInt(raf, order);
340+
int type = readInt(raf, order);
341+
342+
if (namesz < 0 || descsz < 0 || namesz > 256) {
343+
break;
344+
}
345+
346+
int nameAligned = align4(namesz);
347+
long descPos = pos + 12 + nameAligned;
348+
349+
if (namesz > 0 && type == NT_GNU_BUILD_ID && descPos + descsz <= end) {
350+
byte[] nameBytes = new byte[namesz];
351+
raf.readFully(nameBytes);
352+
// Name is null-terminated.
353+
String name =
354+
namesz > 0 ? new String(nameBytes, 0, Math.max(0, namesz - 1), "US-ASCII") : "";
355+
356+
if (GNU_NOTE_NAME.equals(name) && descsz > 0) {
357+
raf.seek(descPos);
358+
byte[] desc = new byte[descsz];
359+
raf.readFully(desc);
360+
return bytesToHex(desc);
361+
}
362+
}
363+
364+
pos = descPos + align4(descsz);
365+
}
366+
return null;
367+
}
368+
369+
private static int align4(int value) {
370+
return (value + 3) & ~3;
371+
}
372+
373+
private static int readInt(RandomAccessFile raf, ByteOrder order) throws Exception {
374+
byte[] buf = new byte[4];
375+
raf.readFully(buf);
376+
return ByteBuffer.wrap(buf).order(order).getInt();
377+
}
378+
379+
private static long readLong(RandomAccessFile raf, ByteOrder order) throws Exception {
380+
byte[] buf = new byte[8];
381+
raf.readFully(buf);
382+
return ByteBuffer.wrap(buf).order(order).getLong();
383+
}
384+
385+
private static int readUnsignedShort(RandomAccessFile raf, ByteOrder order) throws Exception {
386+
byte[] buf = new byte[2];
387+
raf.readFully(buf);
388+
return ByteBuffer.wrap(buf).order(order).getShort() & 0xFFFF;
389+
}
390+
391+
private static String bytesToHex(byte[] bytes) {
392+
StringBuilder sb = new StringBuilder(bytes.length * 2);
393+
for (byte b : bytes) {
394+
sb.append(String.format("%02x", b & 0xff));
395+
}
396+
return sb.toString();
397+
}
398+
}

0 commit comments

Comments
 (0)