Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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 @@ -10,6 +10,7 @@

- Fix screenshots not being captured by hybrid SDKs (#8578)
- 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)
- Remove x/y coordinates from UI breadcrumbs, as they are a potential security risk, for example leaking input on custom PIN code views (#8534)

## 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
23 changes: 23 additions & 0 deletions Sources/Sentry/SentryObjCExceptionHelper.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#import "SentryObjCExceptionHelper.h"
#import "SentryLogC.h"

@implementation SentryObjCExceptionHelper

+ (BOOL)tryBlock:(NS_NOESCAPE void (^)(void))block
catchingExceptionWithName:(NSExceptionName)name
reasonPrefix:(NSString *)reasonPrefix
{
@try {
block();
return YES;
} @catch (NSException *exception) {
Comment thread
philprime marked this conversation as resolved.
Comment thread
philprime marked this conversation as resolved.
if (![exception.name isEqualToString:name] || ![exception.reason hasPrefix:reasonPrefix]) {
Comment thread
philprime marked this conversation as resolved.
@throw;
}

SENTRY_LOG_WARN(@"Caught Objective-C exception %@: %@", exception.name, exception.reason);
return NO;
}
}

@end
31 changes: 31 additions & 0 deletions Sources/Sentry/include/SentryObjCExceptionHelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#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.
/// - Parameter name: The name of the exception to catch.
/// - Parameter reasonPrefix: The prefix of the exception reason to catch.
/// - Returns: `YES` if `block` completed without raising an `NSException`, `NO` if a matching
/// exception was caught and swallowed. Non-matching exceptions are rethrown.
+ (BOOL)tryBlock:(NS_NOESCAPE void (^)(void))block
catchingExceptionWithName:(NSExceptionName)name
reasonPrefix:(NSString *)reasonPrefix
NS_SWIFT_NAME(tryBlock(_:catchingExceptionWithName:reasonPrefix:));

@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 @@ -25,6 +25,7 @@
#import "SentryNSFileManagerSwizzlingHelper.h"
#import "SentryNSURLSessionTaskSearch.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)
internal import _SentryPrivate
import Foundation
import ObjectiveC.NSObjCRuntime
import UIKit
Expand Down Expand Up @@ -114,7 +115,7 @@ final class SentryUIRedactBuilder {
/// strings, the subtree will be ignored. For example, "MyView" will match "MyApp.MyView",
/// "MyViewSubclass", etc.
private var excludedViewClassPatterns: Set<String>

/// A set of view type identifier strings that should be included in subtree traversal.
///
/// Views exactly matching these strings will be removed from the excluded set, allowing their subtrees
Expand Down Expand Up @@ -246,7 +247,7 @@ final class SentryUIRedactBuilder {
excludedViewClassPatterns = SentryViewSubtreeTraversal.defaultExcludedViewClassPatterns
.union(options.excludedViewClasses)
includedViewClassPatterns = options.includedViewClasses

// didSet doesn't run during initialization, so we need to manually build the optimization structures
rebuildOptimizedLookups()
}
Expand Down Expand Up @@ -412,7 +413,7 @@ final class SentryUIRedactBuilder {
func isRedactContainerClassTestOnly(_ containerClass: AnyClass) -> Bool {
return isRedactContainerClass(containerClass)
}

func getRedactClassesIdentifiersTestOnly() -> Set<ClassIdentifier> {
redactClassesIdentifiers
}
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,20 @@ 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) else {
Comment thread
philprime marked this conversation as resolved.
// Reading the sublayers raised an exception, so we cannot inspect the subtree to decide what
Comment thread
sentry[bot] marked this conversation as resolved.
// needs redacting. To avoid leaking unmasked content (e.g. text or images in children we could
// not enumerate), we redact the whole layer bounds. This mirrors `isViewSubtreeIgnored`, which
// also redacts the full region when it must skip a crash-prone subtree.
redacting.append(SentryRedactRegion(
size: layer.bounds.size,
transform: newTransform,
type: .redact,
name: type(of: layer).description()
))
return
Comment thread
cursor[bot] marked this conversation as resolved.
}
guard subLayers.count > 0 else {
return
}
let clipToBounds = layer.masksToBounds
Expand Down Expand Up @@ -668,6 +682,48 @@ 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://docs.sentry.io/platforms/apple/guides/ios/session-replay/troubleshooting and
/// https://github.com/getsentry/sentry-cocoa/issues/7810 for more details.
private func safeSublayers(of layer: CALayer) -> [CALayer]? {
var sublayers: [CALayer]?
let succeeded = SentryObjCExceptionHelper.tryBlock({
sublayers = layer.sublayers
}, catchingExceptionWithName: .invalidArgumentException,
reasonPrefix: "-[NSConcreteValue doubleValue]: unrecognized selector sent to instance")
guard succeeded else {
SentrySDKLog.warning("Skipping redaction of a layer subtree because accessing its sublayers raised an exception. See https://docs.sentry.io/platforms/apple/guides/ios/session-replay/troubleshooting and https://github.com/getsentry/sentry-cocoa/issues/7810 for more details.")
return nil
}
return sublayers ?? []
}

/// 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()
}, catchingExceptionWithName: .invalidArgumentException,
reasonPrefix: "-[NSConcreteValue doubleValue]: unrecognized selector sent to instance")
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 Expand Up @@ -742,7 +798,7 @@ final class SentryUIRedactBuilder {
/// were incorrectly treated as opaque, causing text behind them to not be redacted.
/// See: https://github.com/getsentry/sentry-cocoa/pull/6629#issuecomment-3479730690
private func isOpaque(_ view: UIView) -> Bool {
let layer = view.layer.presentation() ?? view.layer
let layer = safePresentationLayer(of: view.layer) ?? view.layer

// Allow explicit override: if a view is marked to clip out, treat it as opaque
if SentryRedactViewHelper.shouldClipOut(view) {
Expand Down
Loading
Loading