Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixes

- Fix trace propagation for manually instrumented transactions when automatic performance tracing is disabled (#8522)
- Prevent a Session Replay crash (`NSInvalidArgumentException` / `-[NSConcreteValue doubleValue]`) when Core Animation raises while redacting the view hierarchy, e.g. during React Navigation transitions or video fullscreen presentation (#8537)

## 9.23.0

Expand Down
12 changes: 12 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,10 @@
D3D97CB7DB9C0D43EF5C4185 /* SentryInternalDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 627E7588299F6FE40085504D /* SentryInternalDefines.h */; };
D3DEBC6E35E03BCA1FDC1BD2 /* SentrySpanInternal+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 84A789092C0E9F5800FF0803 /* SentrySpanInternal+Private.h */; };
D41A231B2F3B2A3800F279CA /* SentrySwiftUIExports.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41A231A2F3B2A3800F279C9 /* SentrySwiftUIExports.swift */; };
D431C8BB301251A000A2F0DE /* SentryObjCExceptionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D431C8BA301251A000A2F0DE /* SentryObjCExceptionHelper.m */; };
D431C8BC301251A000A2F0DE /* SentryObjCExceptionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D431C8BA301251A000A2F0DE /* SentryObjCExceptionHelper.m */; };
D431C8C3301251AE00A2F0DE /* SentryObjCExceptionHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D431C8C2301251AE00A2F0DE /* SentryObjCExceptionHelper.h */; };
D431C8C4301251AE00A2F0DE /* SentryObjCExceptionHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D431C8C2301251AE00A2F0DE /* SentryObjCExceptionHelper.h */; };
D43A2A102DD47FB700114724 /* SentryWeakMap.h in Headers */ = {isa = PBXBuildFile; fileRef = D43A2A0F2DD47FB700114724 /* SentryWeakMap.h */; };
D43A2A122DD47FCE00114724 /* SentryWeakMap.m in Sources */ = {isa = PBXBuildFile; fileRef = D43A2A112DD47FCE00114724 /* SentryWeakMap.m */; };
D43B0E602DE7416900EE3759 /* TestFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43B0E5F2DE7416600EE3759 /* TestFileManagerTests.swift */; };
Expand Down Expand Up @@ -1570,6 +1574,8 @@
D269D1832ED1DD86DF4AF78A /* SentryObjC.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SentryObjC.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D36813946B580095D0A8C6D2 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
D41A231A2F3B2A3800F279C9 /* SentrySwiftUIExports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySwiftUIExports.swift; sourceTree = "<group>"; };
D431C8BA301251A000A2F0DE /* SentryObjCExceptionHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryObjCExceptionHelper.m; sourceTree = "<group>"; };
D431C8C2301251AE00A2F0DE /* SentryObjCExceptionHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryObjCExceptionHelper.h; path = include/SentryObjCExceptionHelper.h; sourceTree = "<group>"; };
D43A2A0F2DD47FB700114724 /* SentryWeakMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryWeakMap.h; path = include/SentryWeakMap.h; sourceTree = "<group>"; };
D43A2A112DD47FCE00114724 /* SentryWeakMap.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryWeakMap.m; sourceTree = "<group>"; };
D43B0E5D2DE7198000EE3759 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2310,6 +2316,8 @@
7D082B8023C628780029866B /* SentryMeta.m */,
D4ECA3FF2E3CBEDE00C757EA /* SentryDummyPrivateEmptyClass.m */,
D4ECA4002E3CBEDE00C757EA /* SentryDummyPublicEmptyClass.m */,
D431C8C2301251AE00A2F0DE /* SentryObjCExceptionHelper.h */,
D431C8BA301251A000A2F0DE /* SentryObjCExceptionHelper.m */,
);
path = Sentry;
sourceTree = "<group>";
Expand Down Expand Up @@ -3071,6 +3079,7 @@
F42F49BD2F2903B400903377 /* SentryCoreDataSwizzlingHelper.h in Headers */,
D456B4322D706BDF007068CB /* SentrySpanOperation.h in Headers */,
63FE714D20DA4C1100CDBAE8 /* SentryCrashJSONCodec.h in Headers */,
D431C8C4301251AE00A2F0DE /* SentryObjCExceptionHelper.h in Headers */,
FAD882C42EDB3F4C0055AA44 /* SentryCrashAsync.h in Headers */,
7BAF3DD4243DD40F008A5414 /* SentryTransportFactory.h in Headers */,
63FE717F20DA4C1100CDBAE8 /* SentryCrashReportFields.h in Headers */,
Expand Down Expand Up @@ -3347,6 +3356,7 @@
C46D51C8E4BA24B9AF69BC23 /* SentryCoreDataSwizzlingHelper.h in Headers */,
85E17D3D4E80EC59C0C17250 /* SentrySpanOperation.h in Headers */,
D866B182DB131B27154293DA /* SentryCrashJSONCodec.h in Headers */,
D431C8C3301251AE00A2F0DE /* SentryObjCExceptionHelper.h in Headers */,
3533350D6A03DFC88A17FFD8 /* SentryCrashAsync.h in Headers */,
3B16D53EEF080920D9B37CDF /* SentryTransportFactory.h in Headers */,
79F028DAF6326E63A0C21776 /* SentryCrashReportFields.h in Headers */,
Expand Down Expand Up @@ -4089,6 +4099,7 @@
7BFC16A125249A9D00FF6266 /* SentryMessage.m in Sources */,
7BCFBD6F2681D0EE00BC27D8 /* SentryCrashScopeObserver.m in Sources */,
7BD86ED1264A7CF6005439DB /* SentryAppStartMeasurement.m in Sources */,
D431C8BC301251A000A2F0DE /* SentryObjCExceptionHelper.m in Sources */,
63FE717B20DA4C1100CDBAE8 /* SentryCrashReport.c in Sources */,
7B3398652459C15200BD9C96 /* SentryEnvelopeRateLimit.m in Sources */,
7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */,
Expand Down Expand Up @@ -4321,6 +4332,7 @@
906677EC8729A26977A30F64 /* SentryMessage.m in Sources */,
1238DF7922E9CBD16A4A62CA /* SentryCrashScopeObserver.m in Sources */,
32232D58C9A6629EC064F913 /* SentryAppStartMeasurement.m in Sources */,
D431C8BB301251A000A2F0DE /* SentryObjCExceptionHelper.m in Sources */,
FF35F5FDB86024EBD89EC583 /* SentryCrashReport.c in Sources */,
EB14FC5AF814DA1F8A69E85F /* SentryEnvelopeRateLimit.m in Sources */,
960CC78F3C69CEE67F1A645F /* NSMutableDictionary+Sentry.m in Sources */,
Expand Down
15 changes: 15 additions & 0 deletions Sources/Sentry/SentryObjCExceptionHelper.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#import "SentryObjCExceptionHelper.h"

@implementation SentryObjCExceptionHelper

+ (BOOL)tryBlock:(NS_NOESCAPE void (^)(void))block
{
@try {
block();
return YES;
} @catch (NSException *exception) {
Comment thread
philprime marked this conversation as resolved.
Comment thread
philprime marked this conversation as resolved.
return NO;
}
}

@end
26 changes: 26 additions & 0 deletions Sources/Sentry/include/SentryObjCExceptionHelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/// Helper to run a block inside an Objective-C `@try`/`@catch`.
///
/// Swift cannot catch Objective-C exceptions. Some system frameworks (notably Core Animation)
/// raise `NSException`s from code paths the SDK has to touch β€” for example resolving a layer's
/// presentation layer while a malformed `CABasicAnimation` is running raises
/// `-[NSConcreteValue doubleValue]: unrecognized selector`. When such an exception unwinds through
/// Swift frames it force-kills the host app. This helper lets Swift callers run the risky access
/// inside an Objective-C exception handler so they can degrade gracefully instead of crashing.
///
/// See https://github.com/getsentry/sentry-cocoa/issues/7810 for context.
@interface SentryObjCExceptionHelper : NSObject

/// Executes `block` inside an Objective-C `@try`/`@catch`.
///
/// - Parameter block: The block to execute. It is run synchronously before this method returns.
/// - Returns: `YES` if `block` completed without raising an `NSException`, `NO` if an exception
/// was caught and swallowed.
+ (BOOL)tryBlock:(NS_NOESCAPE void (^)(void))block NS_SWIFT_NAME(tryBlock(_:));

@end

NS_ASSUME_NONNULL_END
1 change: 1 addition & 0 deletions Sources/Sentry/include/SentryPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#import "SentryNSExceptionCaptureHelper.h"
#import "SentryNSFileManagerSwizzlingHelper.h"
#import "SentryNetworkTracker.h"
#import "SentryObjCExceptionHelper.h"
#import "SentrySDK+Private.h"
#import "SentryScope+Private.h"
#import "SentrySessionReplaySyncC.h"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// swiftlint:disable file_length type_body_length
#if canImport(UIKit) && !SENTRY_NO_UI_FRAMEWORK
#if os(iOS) || os(tvOS)
@_implementationOnly import _SentryPrivate
import Foundation
import ObjectiveC.NSObjCRuntime
import UIKit
Expand Down Expand Up @@ -448,7 +449,7 @@ final class SentryUIRedactBuilder {
var redactingRegions = [SentryRedactRegion]()

self.mapRedactRegion(
fromLayer: view.layer.presentation() ?? view.layer,
fromLayer: safePresentationLayer(of: view.layer) ?? view.layer,
Comment thread
cursor[bot] marked this conversation as resolved.
relativeTo: nil,
redacting: &redactingRegions,
rootFrame: view.frame,
Comment thread
philprime marked this conversation as resolved.
Comment thread
philprime marked this conversation as resolved.
Expand Down Expand Up @@ -626,7 +627,7 @@ final class SentryUIRedactBuilder {
}

// Traverse the sublayers to redact them if necessary
guard let subLayers = layer.sublayers, subLayers.count > 0 else {
guard let subLayers = safeSublayers(of: layer), subLayers.count > 0 else {
Comment thread
philprime marked this conversation as resolved.
Outdated
return
}
let clipToBounds = layer.masksToBounds
Expand Down Expand Up @@ -668,6 +669,45 @@ final class SentryUIRedactBuilder {
}
}

/// Reads `layer.sublayers` behind an Objective-C exception handler.
///
/// Accessing `sublayers` on a presentation layer makes Core Animation resolve the presentation
/// layers of its children, interpolating any running animations. A malformed animation β€” e.g. a
/// `CABasicAnimation` whose endpoints mix a scalar `NSNumber` with an `NSValue` boxing a struct β€”
/// makes Core Animation raise an `NSException` (`-[NSConcreteValue doubleValue]: unrecognized
/// selector`). Swift cannot catch that, so it would unwind through these frames and force-kill
/// the host app. Taking a Session Replay screenshot must never crash the app, so we route the
/// access through an Objective-C exception handler and skip the subtree if it throws.
///
/// See https://github.com/getsentry/sentry-cocoa/issues/7810.
private func safeSublayers(of layer: CALayer) -> [CALayer]? {
var sublayers: [CALayer]?
let succeeded = SentryObjCExceptionHelper.tryBlock {
sublayers = layer.sublayers
}
guard succeeded else {
SentrySDKLog.warning("Skipping redaction of a layer subtree because accessing its sublayers raised an exception. See https://github.com/getsentry/sentry-cocoa/issues/7810")
Comment thread
philprime marked this conversation as resolved.
Outdated
return nil
}
return sublayers
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
}

/// Reads `layer.presentation()` behind an Objective-C exception handler.
///
/// Resolving the presentation layer can raise for the same reason as `safeSublayers(of:)`.
/// Returns `nil` if it throws, so callers can fall back to the model layer.
private func safePresentationLayer(of layer: CALayer) -> CALayer? {
var presentationLayer: CALayer?
let succeeded = SentryObjCExceptionHelper.tryBlock {
presentationLayer = layer.presentation()
}
guard succeeded else {
SentrySDKLog.warning("Failed to access a presentation layer because it raised an exception; falling back to the model layer. See https://github.com/getsentry/sentry-cocoa/issues/7810")
return nil
}
return presentationLayer
}

private func isViewSubtreeIgnored(_ view: UIView) -> Bool {
if SentryViewSubtreeTraversal.isExcluded(
view,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#if os(iOS) && !targetEnvironment(macCatalyst)
@_spi(Private) @testable import Sentry
import Foundation
import SentryTestUtils
import UIKit
import XCTest

/// Reproduction for https://github.com/getsentry/sentry-cocoa/issues/7810
///
/// Session Replay crashes with `-[NSConcreteValue doubleValue]: unrecognized selector`
/// inside `SentryUIRedactBuilder.mapRedactRegion`. The crash originates from Core Animation
/// while `mapRedactRegion` walks the layer tree via `layer.sublayers`: resolving the
/// presentation layer of a layer that has an active `CABasicAnimation` whose value is a
/// boxed struct (`NSConcreteValue` wrapping `CGRect`/`CGPoint`/`CATransform3D`) makes Core
/// Animation send `doubleValue` to a value that does not respond to it, raising an
/// `NSInvalidArgumentException`.
///
/// The exception unwinds through the Swift frames of `mapRedactRegion` β€” which cannot catch
/// Objective-C exceptions β€” and force-kills the app.
///
/// These tests simulate that failure deterministically by overriding `CALayer.sublayers`
/// to raise the exact `NSInvalidArgumentException` at the crash site.
///
/// See `SentryUIRedactBuilderTests.swift` for more information on how to print the internal
/// view hierarchy of a view.
class SentryUIRedactBuilderTests_LayerTraversalCrash: SentryUIRedactBuilderTests { // swiftlint:disable:this type_name

/// A `CALayer` whose `sublayers` getter raises the same `NSInvalidArgumentException`
/// that Core Animation raises when interpolating a struct-valued animation as a scalar.
private final class ThrowingSublayersLayer: CALayer {
override var sublayers: [CALayer]? {
get {
NSException(
name: .invalidArgumentException,
reason: "-[NSConcreteValue doubleValue]: unrecognized selector sent to instance 0x12e68f150",
userInfo: nil
).raise()
return nil
}
set {
// No-op: this layer never exposes sublayers, it only crashes on read.
}
}
}

/// A plain `UIView` backed by ``ThrowingSublayersLayer`` so that traversing its layer
/// subtree reproduces the crash. It intentionally is not text/image content, so it is
/// neither redacted nor ignored on its own merits.
private final class ThrowingSublayersView: UIView {
override class var layerClass: AnyClass { ThrowingSublayersLayer.self }
}

private func getSut(maskAllText: Bool, maskAllImages: Bool) -> SentryUIRedactBuilder {
return SentryUIRedactBuilder(options: TestRedactOptions(
maskAllText: maskAllText,
maskAllImages: maskAllImages
))
}

func testRedactRegionsFor_whenSublayersAccessThrows_shouldNotCrash() throws {
// -- Arrange --
let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))

// A label that must still be redacted even though a sibling subtree crashes.
let label = UILabel(frame: CGRect(x: 10, y: 10, width: 80, height: 20))
label.text = "Secret Text"
label.textColor = .purple
rootView.addSubview(label)

// A view whose layer raises when its sublayers are accessed, mimicking the
// Core Animation crash during a running struct-valued animation.
let throwingView = ThrowingSublayersView(frame: CGRect(x: 0, y: 40, width: 100, height: 40))
rootView.addSubview(throwingView)

// View Hierarchy:
// ---------------
// <UIView: 0x...; frame = (0 0; 100 100); layer = <CALayer: 0x...>>
// | <UILabel: 0x...; frame = (10 10; 80 20); layer = <_UILabelLayer: 0x...>>
// | <ThrowingSublayersView: 0x...; frame = (0 40; 100 40); layer = <ThrowingSublayersLayer: 0x...>>

// -- Act --
let sut = getSut(maskAllText: true, maskAllImages: true)
let result = sut.redactRegionsFor(view: rootView)

// -- Assert --
// Traversal of the throwing subtree must be skipped gracefully instead of crashing,
// and the sibling label must still be redacted.
let labelRegions = result.filter { $0.type == .redact && $0.color == UIColor.purple }
XCTAssertEqual(labelRegions.count, 1, "Sibling label should still be redacted despite the crashing subtree")

let labelRegion = try XCTUnwrap(labelRegions.first)
XCTAssertEqual(labelRegion.size, CGSize(width: 80, height: 20))
XCTAssertEqual(labelRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 10, ty: 10))
}

func testRedactRegionsFor_whenRootSublayersAccessThrows_shouldNotCrash() {
// -- Arrange --
// The root view itself is backed by a layer whose sublayers access throws.
let rootView = ThrowingSublayersView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))

// -- Act --
let sut = getSut(maskAllText: true, maskAllImages: true)
let result = sut.redactRegionsFor(view: rootView)

// -- Assert --
// Should not crash. No regions can be collected because traversal stops at the root.
XCTAssertEqual(result.count, 0)
}
}

#endif // os(iOS) && !targetEnvironment(macCatalyst)
Loading
Loading