Skip to content

Commit 8c7718c

Browse files
romtsnclaude
andauthored
fix(replay): Fix VerifyError in Compose masking under DexGuard/R8 obfuscation (#5507)
* fix(replay): Fix VerifyError in Compose masking under DexGuard/R8 obfuscation ComposeViewHierarchyNode.boundsInWindow returned an android.graphics.Rect while the surrounding code carried it as androidx.compose.ui.geometry.Rect, mixing the two Rect types in the same method. Under aggressive obfuscation (DexGuard 9.13.2 / R8 full mode) this could be rejected at class load with a VerifyError, crashing Replay when traversing the Compose tree. Make boundsInWindow return androidx.compose.ui.geometry.Rect throughout and add a Rect.toRect() extension to convert to android.graphics.Rect only at the boundary where the view-hierarchy node needs it. Fixes #5497 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * changelog * fix(replay): Round Compose mask bounds outward to avoid zero-area masks isVisible/shouldMask are derived from the sub-pixel float bounds, but the android.graphics.Rect stored on the node (and drawn by MaskRenderer) used truncating toInt(). A sub-pixel node could be marked visible+maskable yet store a zero-width/height rect, so the mask wasn't drawn and sensitive content leaked. Round outward (floor min, ceil max) so a non-empty float rect always yields a non-empty integer rect, biasing toward over-masking. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent abcd889 commit 8c7718c

3 files changed

Lines changed: 36 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- Session Replay: Fix `VerifyError` in Compose masking under DexGuard/R8 obfuscation ([#5507](https://github.com/getsentry/sentry-java/pull/5507))
78
- Session Replay: Fix Compose view masking not working on obfuscated/minified builds ([#5503](https://github.com/getsentry/sentry-java/pull/5503))
89

910
## 8.43.1

sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22

33
package io.sentry.android.replay.util
44

5-
import android.graphics.Rect
65
import androidx.compose.ui.geometry.Offset
6+
import androidx.compose.ui.geometry.Rect
77
import androidx.compose.ui.graphics.Color
88
import androidx.compose.ui.graphics.ColorProducer
99
import androidx.compose.ui.graphics.painter.Painter
1010
import androidx.compose.ui.layout.LayoutCoordinates
1111
import androidx.compose.ui.layout.findRootCoordinates
1212
import androidx.compose.ui.node.LayoutNode
1313
import androidx.compose.ui.text.TextLayoutResult
14+
import kotlin.math.ceil
15+
import kotlin.math.floor
1416
import kotlin.math.roundToInt
1517

1618
internal class ComposeTextLayout(internal val layout: TextLayoutResult) : TextLayout {
@@ -176,7 +178,7 @@ internal fun LayoutCoordinates.boundsInWindow(rootCoordinates: LayoutCoordinates
176178
val boundsBottom = bounds.bottom.fastCoerceIn(0f, rootHeight)
177179

178180
if (boundsLeft == boundsRight || boundsTop == boundsBottom) {
179-
return Rect()
181+
return Rect(0.0f, 0.0f, 0.0f, 0.0f)
180182
}
181183

182184
val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
@@ -200,5 +202,18 @@ internal fun LayoutCoordinates.boundsInWindow(rootCoordinates: LayoutCoordinates
200202
val top = fastMinOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
201203
val bottom = fastMaxOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
202204

203-
return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
205+
return Rect(left, top, right, bottom)
206+
}
207+
208+
internal fun Rect.toRect(): android.graphics.Rect {
209+
// Round outward (floor min edges, ceil max edges) so that a sub-pixel but non-empty Rect doesn't
210+
// collapse to a zero-width/height android.graphics.Rect. Otherwise a node could be marked visible
211+
// and maskable based on the float bounds, while the integer rect the MaskRenderer draws has zero
212+
// area, leaving sensitive content unmasked. Rounding outward also biases toward over-masking.
213+
return android.graphics.Rect(
214+
floor(left).toInt(),
215+
floor(top).toInt(),
216+
ceil(right).toInt(),
217+
ceil(bottom).toInt(),
218+
)
204219
}

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import io.sentry.android.replay.util.findPainter
2828
import io.sentry.android.replay.util.findTextColor
2929
import io.sentry.android.replay.util.isMaskable
3030
import io.sentry.android.replay.util.toOpaque
31+
import io.sentry.android.replay.util.toRect
3132
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode
3233
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
3334
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
@@ -157,8 +158,8 @@ internal object ComposeViewHierarchyNode {
157158
// If we're unable to retrieve the semantics configuration
158159
// we should play safe and mask the whole node.
159160
return GenericViewHierarchyNode(
160-
x = visibleRect.left.toFloat(),
161-
y = visibleRect.top.toFloat(),
161+
x = visibleRect.left,
162+
y = visibleRect.top,
162163
width = node.width,
163164
height = node.height,
164165
elevation = (parent?.elevation ?: 0f),
@@ -168,17 +169,17 @@ internal object ComposeViewHierarchyNode {
168169
isImportantForContentCapture = false, // will be set by children
169170
isVisible =
170171
!SentryLayoutNodeHelper.isTransparent(node) &&
171-
visibleRect.height() > 0 &&
172-
visibleRect.width() > 0,
173-
visibleRect = visibleRect,
172+
visibleRect.height > 0 &&
173+
visibleRect.width > 0,
174+
visibleRect = visibleRect.toRect(),
174175
)
175176
}
176177

177178
val isVisible =
178179
!SentryLayoutNodeHelper.isTransparent(node) &&
179180
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
180-
visibleRect.height() > 0 &&
181-
visibleRect.width() > 0
181+
visibleRect.height > 0 &&
182+
visibleRect.width > 0
182183
val isEditable =
183184
semantics?.contains(SemanticsActions.SetText) == true ||
184185
semantics?.contains(SemanticsProperties.EditableText) == true
@@ -213,8 +214,8 @@ internal object ComposeViewHierarchyNode {
213214
null
214215
},
215216
dominantColor = textColor?.toArgb()?.toOpaque(),
216-
x = visibleRect.left.toFloat(),
217-
y = visibleRect.top.toFloat(),
217+
x = visibleRect.left,
218+
y = visibleRect.top,
218219
width = node.width,
219220
height = node.height,
220221
elevation = (parent?.elevation ?: 0f),
@@ -223,7 +224,7 @@ internal object ComposeViewHierarchyNode {
223224
shouldMask = shouldMask,
224225
isImportantForContentCapture = true,
225226
isVisible = isVisible,
226-
visibleRect = visibleRect,
227+
visibleRect = visibleRect.toRect(),
227228
)
228229
}
229230
else -> {
@@ -233,8 +234,8 @@ internal object ComposeViewHierarchyNode {
233234

234235
parent?.setImportantForCaptureToAncestors(true)
235236
ImageViewHierarchyNode(
236-
x = visibleRect.left.toFloat(),
237-
y = visibleRect.top.toFloat(),
237+
x = visibleRect.left,
238+
y = visibleRect.top,
238239
width = node.width,
239240
height = node.height,
240241
elevation = (parent?.elevation ?: 0f),
@@ -243,7 +244,7 @@ internal object ComposeViewHierarchyNode {
243244
isVisible = isVisible,
244245
isImportantForContentCapture = true,
245246
shouldMask = shouldMask && painter.isMaskable(),
246-
visibleRect = visibleRect,
247+
visibleRect = visibleRect.toRect(),
247248
)
248249
} else {
249250
val shouldMask = isVisible && semantics.shouldMask(isImage = false, options)
@@ -252,8 +253,8 @@ internal object ComposeViewHierarchyNode {
252253
// TODO: traverse the ViewHierarchyNode here again. For now we can recommend
253254
// TODO: using custom modifiers to obscure the entire node if it's sensitive
254255
GenericViewHierarchyNode(
255-
x = visibleRect.left.toFloat(),
256-
y = visibleRect.top.toFloat(),
256+
x = visibleRect.left,
257+
y = visibleRect.top,
257258
width = node.width,
258259
height = node.height,
259260
elevation = (parent?.elevation ?: 0f),
@@ -262,7 +263,7 @@ internal object ComposeViewHierarchyNode {
262263
shouldMask = shouldMask,
263264
isImportantForContentCapture = false, // will be set by children
264265
isVisible = isVisible,
265-
visibleRect = visibleRect,
266+
visibleRect = visibleRect.toRect(),
266267
)
267268
}
268269
}

0 commit comments

Comments
 (0)