Skip to content

Commit d4bc8e0

Browse files
buenaflorgetsentry-botmarkushi
committed
feat(android-ndk): add api for getting debug images by addresses (#4089)
--------- Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io> Co-authored-by: Markus Hintersteiner <markus.hintersteiner@sentry.io>
1 parent 1eac2fc commit d4bc8e0

File tree

8 files changed

+216
-5
lines changed

8 files changed

+216
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
- (Jetpack Compose) Modifier.sentryTag now uses Modifier.Node ([#4029](https://github.com/getsentry/sentry-java/pull/4029))
1212
- This allows Composables that use this modifier to be skippable
1313

14+
### Features
15+
16+
- (Internal) Add API to filter native debug images based on stacktrace addresses ([#4089](https://github.com/getsentry/sentry-java/pull/4089))
17+
1418
## 7.21.0
1519

1620
### Fixes

sentry-android-core/api/sentry-android-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : i
208208
public abstract interface class io/sentry/android/core/IDebugImagesLoader {
209209
public abstract fun clearDebugImages ()V
210210
public abstract fun loadDebugImages ()Ljava/util/List;
211+
public abstract fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set;
211212
}
212213

213214
public final class io/sentry/android/core/InternalSentrySdk {

sentry-android-core/src/main/java/io/sentry/android/core/IDebugImagesLoader.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.sentry.protocol.DebugImage;
44
import java.util.List;
5+
import java.util.Set;
56
import org.jetbrains.annotations.ApiStatus;
67
import org.jetbrains.annotations.Nullable;
78

@@ -11,5 +12,8 @@ public interface IDebugImagesLoader {
1112
@Nullable
1213
List<DebugImage> loadDebugImages();
1314

15+
@Nullable
16+
Set<DebugImage> loadDebugImagesForAddresses(Set<String> addresses);
17+
1418
void clearDebugImages();
1519
}

sentry-android-core/src/main/java/io/sentry/android/core/NoOpDebugImagesLoader.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.sentry.protocol.DebugImage;
44
import java.util.List;
5+
import java.util.Set;
56
import org.jetbrains.annotations.Nullable;
67

78
final class NoOpDebugImagesLoader implements IDebugImagesLoader {
@@ -19,6 +20,11 @@ public static NoOpDebugImagesLoader getInstance() {
1920
return null;
2021
}
2122

23+
@Override
24+
public @Nullable Set<DebugImage> loadDebugImagesForAddresses(Set<String> addresses) {
25+
return null;
26+
}
27+
2228
@Override
2329
public void clearDebugImages() {}
2430
}

sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ class SentryAndroidOptionsTest {
182182

183183
private class CustomDebugImagesLoader : IDebugImagesLoader {
184184
override fun loadDebugImages(): List<DebugImage>? = null
185+
override fun loadDebugImagesForAddresses(addresses: Set<String>?): Set<DebugImage>? = null
186+
185187
override fun clearDebugImages() {}
186188
}
187189
}

sentry-android-ndk/api/sentry-android-ndk.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public final class io/sentry/android/ndk/DebugImagesLoader : io/sentry/android/c
1010
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/ndk/NativeModuleListLoader;)V
1111
public fun clearDebugImages ()V
1212
public fun loadDebugImages ()Ljava/util/List;
13+
public fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set;
1314
}
1415

1516
public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/ScopeObserverAdapter {

sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import io.sentry.android.core.SentryAndroidOptions;
77
import io.sentry.protocol.DebugImage;
88
import io.sentry.util.Objects;
9-
import java.util.Arrays;
9+
import java.util.ArrayList;
10+
import java.util.HashSet;
1011
import java.util.List;
12+
import java.util.Set;
1113
import org.jetbrains.annotations.NotNull;
1214
import org.jetbrains.annotations.Nullable;
1315
import org.jetbrains.annotations.VisibleForTesting;
@@ -22,7 +24,7 @@ public final class DebugImagesLoader implements IDebugImagesLoader {
2224

2325
private final @NotNull NativeModuleListLoader moduleListLoader;
2426

25-
private static @Nullable List<DebugImage> debugImages;
27+
private static volatile @Nullable List<DebugImage> debugImages;
2628

2729
/** we need to lock it because it could be called from different threads */
2830
private static final @NotNull Object debugImagesLock = new Object();
@@ -60,7 +62,92 @@ public DebugImagesLoader(
6062
return debugImages;
6163
}
6264

63-
/** Clears the caching of debug images on sentry-native and here. */
65+
/**
66+
* Loads debug images for the given set of addresses.
67+
*
68+
* @param addresses Set of memory addresses to find debug images for
69+
* @return Set of matching debug images, or null if debug images couldn't be loaded
70+
*/
71+
public @Nullable Set<DebugImage> loadDebugImagesForAddresses(
72+
final @NotNull Set<String> addresses) {
73+
try (final @NotNull ISentryLifecycleToken ignored = debugImagesLock.acquire()) {
74+
final @Nullable List<DebugImage> allDebugImages = loadDebugImages();
75+
if (allDebugImages == null) {
76+
return null;
77+
}
78+
if (addresses.isEmpty()) {
79+
return null;
80+
}
81+
82+
final Set<DebugImage> referencedImages = filterImagesByAddresses(allDebugImages, addresses);
83+
if (referencedImages.isEmpty()) {
84+
options
85+
.getLogger()
86+
.log(
87+
SentryLevel.WARNING,
88+
"No debug images found for any of the %d addresses.",
89+
addresses.size());
90+
return null;
91+
}
92+
93+
return referencedImages;
94+
}
95+
}
96+
97+
/**
98+
* Finds all debug image containing the given addresses. Assumes that the images are sorted by
99+
* address, which should always be true on Linux/Android and Windows platforms
100+
*
101+
* @return All matching debug images or null if none are found
102+
*/
103+
private @NotNull Set<DebugImage> filterImagesByAddresses(
104+
final @NotNull List<DebugImage> images, final @NotNull Set<String> addresses) {
105+
final Set<DebugImage> result = new HashSet<>();
106+
107+
for (int i = 0; i < images.size(); i++) {
108+
final @NotNull DebugImage image = images.get(i);
109+
final @Nullable DebugImage nextDebugImage =
110+
(i + 1) < images.size() ? images.get(i + 1) : null;
111+
final @Nullable String nextDebugImageAddress =
112+
nextDebugImage != null ? nextDebugImage.getImageAddr() : null;
113+
114+
for (final @NotNull String rawAddress : addresses) {
115+
try {
116+
final long address = Long.parseLong(rawAddress.replace("0x", ""), 16);
117+
118+
final @Nullable String imageAddress = image.getImageAddr();
119+
if (imageAddress != null) {
120+
try {
121+
final long imageStart = Long.parseLong(imageAddress.replace("0x", ""), 16);
122+
final long imageEnd;
123+
124+
final @Nullable Long imageSize = image.getImageSize();
125+
if (imageSize != null) {
126+
imageEnd = imageStart + imageSize;
127+
} else if (nextDebugImageAddress != null) {
128+
imageEnd = Long.parseLong(nextDebugImageAddress.replace("0x", ""), 16);
129+
} else {
130+
imageEnd = Long.MAX_VALUE;
131+
}
132+
if (address >= imageStart && address < imageEnd) {
133+
result.add(image);
134+
// once image is added we can skip the remaining addresses and go straight to the
135+
// next
136+
// image
137+
break;
138+
}
139+
} catch (NumberFormatException e) {
140+
// ignored, invalid debug image address
141+
}
142+
}
143+
} catch (NumberFormatException e) {
144+
// ignored, invalid address supplied
145+
}
146+
}
147+
}
148+
return result;
149+
}
150+
64151
@Override
65152
public void clearDebugImages() {
66153
synchronized (debugImagesLock) {

sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.mockito.kotlin.verify
77
import org.mockito.kotlin.whenever
88
import java.lang.RuntimeException
99
import kotlin.test.Test
10+
import kotlin.test.assertEquals
1011
import kotlin.test.assertNotNull
1112
import kotlin.test.assertNull
1213
import kotlin.test.assertTrue
@@ -17,11 +18,13 @@ class DebugImagesLoaderTest {
1718
val options = SentryAndroidOptions()
1819

1920
fun getSut(): DebugImagesLoader {
20-
return DebugImagesLoader(options, nativeLoader)
21+
val loader = DebugImagesLoader(options, nativeLoader)
22+
loader.clearDebugImages()
23+
return loader
2124
}
2225
}
2326

24-
private val fixture = Fixture()
27+
private var fixture = Fixture()
2528

2629
@Test
2730
fun `get images returns image list`() {
@@ -78,4 +81,107 @@ class DebugImagesLoaderTest {
7881

7982
assertNull(sut.cachedDebugImages)
8083
}
84+
85+
@Test
86+
fun `find images by address`() {
87+
val sut = fixture.getSut()
88+
89+
val image1 = io.sentry.ndk.DebugImage().apply {
90+
imageAddr = "0x1000"
91+
imageSize = 0x1000L
92+
}
93+
94+
val image2 = io.sentry.ndk.DebugImage().apply {
95+
imageAddr = "0x2000"
96+
imageSize = 0x1000L
97+
}
98+
99+
val image3 = io.sentry.ndk.DebugImage().apply {
100+
imageAddr = "0x3000"
101+
imageSize = 0x1000L
102+
}
103+
104+
whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2, image3))
105+
106+
val result = sut.loadDebugImagesForAddresses(
107+
setOf("0x1500", "0x2500")
108+
)
109+
110+
assertNotNull(result)
111+
assertEquals(2, result.size)
112+
assertTrue(result.any { it.imageAddr == image1.imageAddr })
113+
assertTrue(result.any { it.imageAddr == image2.imageAddr })
114+
}
115+
116+
@Test
117+
fun `find images with invalid addresses are not added to the result`() {
118+
val sut = fixture.getSut()
119+
120+
val image1 = io.sentry.ndk.DebugImage().apply {
121+
imageAddr = "0x1000"
122+
imageSize = 0x1000L
123+
}
124+
125+
val image2 = io.sentry.ndk.DebugImage().apply {
126+
imageAddr = "0x2000"
127+
imageSize = 0x1000L
128+
}
129+
130+
whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2))
131+
132+
val hexAddresses = setOf("0xINVALID", "0x1500")
133+
val result = sut.loadDebugImagesForAddresses(hexAddresses)
134+
135+
assertEquals(1, result!!.size)
136+
}
137+
138+
@Test
139+
fun `find images by address returns null if result is empty`() {
140+
val sut = fixture.getSut()
141+
142+
val image1 = io.sentry.ndk.DebugImage().apply {
143+
imageAddr = "0x1000"
144+
imageSize = 0x1000L
145+
}
146+
147+
val image2 = io.sentry.ndk.DebugImage().apply {
148+
imageAddr = "0x2000"
149+
imageSize = 0x1000L
150+
}
151+
152+
whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2))
153+
154+
val hexAddresses = setOf("0x100", "0x10500")
155+
val result = sut.loadDebugImagesForAddresses(hexAddresses)
156+
157+
assertNull(result)
158+
}
159+
160+
@Test
161+
fun `invalid image adresses are ignored for loadDebugImagesForAddresses`() {
162+
val sut = fixture.getSut()
163+
164+
val image1 = io.sentry.ndk.DebugImage().apply {
165+
imageAddr = "0xNotANumber"
166+
imageSize = 0x1000L
167+
}
168+
169+
val image2 = io.sentry.ndk.DebugImage().apply {
170+
imageAddr = "0x2000"
171+
imageSize = null
172+
}
173+
174+
val image3 = io.sentry.ndk.DebugImage().apply {
175+
imageAddr = "0x5000"
176+
imageSize = 0x1000L
177+
}
178+
179+
whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2, image3))
180+
181+
val hexAddresses = setOf("0x100", "0x2000", "0x2000", "0x5000")
182+
val result = sut.loadDebugImagesForAddresses(hexAddresses)
183+
184+
assertNotNull(result)
185+
assertEquals(2, result.size)
186+
}
81187
}

0 commit comments

Comments
 (0)