Skip to content

Conversation

philprime
Copy link
Member

@philprime philprime commented Sep 2, 2025

📜 Description

This pull request introduces a targeted workaround in the SentryUIRedactBuilder to handle a crash related to CameraUI.ChromeSwiftUIView on iOS 26 when built with Xcode 16. The workaround ensures that the redaction process skips subtrees of this specific internal camera view to prevent the crash, while maintaining normal redaction behavior for other views.

  • Added a runtime check in SentryUIRedactBuilder to detect and skip subtrees of CameraUI.ChromeSwiftUIView when running on iOS 26+ to prevent crashes from unimplemented initializers. (cameraSwiftUIViewClassObjectId, isViewSubtreeIgnored, redactRegionsFor(view:)) [1] [2] [3]
  • Updated redaction region naming to use view.debugDescription instead of layer.name for more consistent identification. [1] [2]
  • Modified ExtrasViewController.swift to present the camera UI using UIImagePickerController for testing camera permission flows when pressing the button Show Camera UI.
  • Added an UI test triggering the camera UI in CI without crashing to detect regression.

💡 Motivation and Context

Fixes #5647

💚 How did you test it?

  • Added additional unit tests.
  • Compiled the iOS-Swift sample using Xcode 16 and ran the app in iOS 26 Beta 7 on simulator and device. It crashed without the changes but does not crash with the changes.

📝 Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

Full Investigation

The issue could be reproduced when using Xcode 16.4 to run on iOS 26.0 Beta 6 or later, where accessing the layer.subLayers during the redaction geometry calculations while displaying the standard Camera UI caused a crash:

Fatal error: Use of unimplemented initializer 'init(layer:)' for class 'CameraUI.ModeLoupeLayer'
Screenshot 2025-09-09 at 09 41 28

During my investigation I found the class to be part of the private framework CameraUI, located at /Library/Developer/CoreSimulator/Volumes/iOS_23A5326a/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 26.0.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/CameraUI.framework when running on iOS 26.0 Beta 6.

My approach to resolve the issue is excluding the view hierarchy of the camera UI from traversal and instead fully redact the UI, as it can be considered personal information anyways. I found the CameraUI.ModeLoupeLayer to be part of the view hierarchy of the CameraUI.ChromeSwiftUIView, so I use it to decide if the subview hierarchy should be ignored from traversal.

While working on the unit tests to verify the changes redaction algorithm, I encountered issues in creating an instance of the CameraUI.ChromeSwiftUIView as it is a private framework which can not be imported directly into the unit tests.

Instead I tried to find the class using NSClassFromString("CameraUI.ChromeSwiftUIView") which also returned nil because the framework was not yet loaded. I was able to resolve this by creating an instance of UIImagePickerController (the original cause of the crash) which then transitively imports the framework CameraUI.

let cameraUIViewClass: AnyClass = try XCTUnwrap(NSClassFromString("CameraUI.ChromeSwiftUIView"))

To analyse the class, I created the following dump tool to access the available Objective-C runtime information:

func dumpObjC(_ cls: AnyClass) {
    func listMethods(of c: AnyClass, title: String) {
        var count: UInt32 = 0
        guard let list = class_copyMethodList(c, &count) else {
            print("No methods for \(title)")
            return
        }
        print("=== \(title) (\(count)) ===")
        for i in 0..<Int(count) {
            let m = list[i]
            let sel = method_getName(m)
            if let name = NSStringFromSelector(sel) as String? {
                let numArgs = method_getNumberOfArguments(m)
                let typeEnc = String(cString: method_getTypeEncoding(m)!)
                print("- \(name)  args:\(numArgs)  types:\(typeEnc)")
            }
        }
        free(list)
    }
    func listProps(of c: AnyClass, title: String) {
        var count: UInt32 = 0
        if let props = class_copyPropertyList(c, &count) {
            print("=== \(title) properties (\(count)) ===")
            for i in 0..<Int(count) {
                let p = props[i]
                let name = String(cString: property_getName(p))
                let attrs = String(cString: property_getAttributes(p)!)
                print("- \(name)  attrs:\(attrs)")
            }
            free(props)
        }
    }
    func listIvars(of c: AnyClass, title: String) {
        var count: UInt32 = 0
        if let ivars = class_copyIvarList(c, &count) {
            print("=== \(title) ivars (\(count)) ===")
            for i in 0..<Int(count) {
                let iv = ivars[i]
                let name = String(cString: ivar_getName(iv)!)
                let type = String(cString: ivar_getTypeEncoding(iv)!)
                print("- \(name)  type:\(type)  offset:\(ivar_getOffset(iv))")
            }
            free(ivars)
        }
    }

    let meta: AnyClass = object_getClass(cls)! // metaclass (holds class methods)
    print("Class: \(NSStringFromClass(cls))")
    print("Superclass: \(NSStringFromClass(class_getSuperclass(cls) ?? NSObject.self))")
    print("Is meta: \(class_isMetaClass(cls))")
    listMethods(of: cls, title: "Instance methods")
    listMethods(of: meta, title: "Class methods")
    listProps(of: cls, title: "Instance")
    listIvars(of: cls, title: "Instance")
}

This resulted in the following output:

Class: CameraUI.ChromeSwiftUIView
Superclass: UIView
Is meta: false
=== Instance methods (6) ===
- .cxx_destruct  args:2  types:v16@0:8
- initWithCoder:  args:3  types:@24@0:8@16
- initWithFrame:  args:3  types:@48@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16
- hitTest:withEvent:  args:4  types:@40@0:8{CGPoint=dd}16@32
- layoutSubviews  args:2  types:v16@0:8
- pointInside:withEvent:  args:4  types:B40@0:8{CGPoint=dd}16@32
No methods for Class methods
=== Instance ivars (2) ===
- viewModel  type:  offset:408
- scenePhaseView  type:  offset:416

This shows that the initWithCoder and initWithFrame exist as expected, but as shown in the crash message might not be implemented.

My initial approach to get an instance of the view was using the instance of the UIImagePickerController and its subviews for the redaction testing. Unfortunately the image picker controller seems to be empty at the beginning and loads additional views after being displayed - most likely also due to permissions checking.

❌ Instance of UIImagePickerController

My second attempt was using the NSClassFromString to create the reference of the class type to then call it's init method with or without a frame:

let cls: AnyClass = try XCTUnwrap(NSClassFromString("CameraUI.ChromeSwiftUIView"))
let CameraViewType = try XCTUnwrap(cls as? UIView.Type)
print(CameraViewType)

let cameraUIView = CameraViewType.init(frame: .init(origin: .zero, size: .init(width: 40, height: 40)))
print(cameraUIView) // ❌ Fatal error: Use of unimplemented initializer 'init(frame:)' for class 'CameraUI.ChromeSwiftUIView'

let cameraUIView2 = CameraViewType.init()
print(cameraUIView2) // ❌ Fatal error: Use of unimplemented initializer 'init(frame:)' for class 'CameraUI.ChromeSwiftUIView'

let data = try! NSKeyedArchiver.archivedData(withRootObject: [:], requiringSecureCoding: false)
let coder = try! NSKeyedUnarchiver(forReadingFrom: data)
coder.requiresSecureCoding = false
let cameraUIView3 = try XCTUnwrap(CameraViewType.init(coder: coder))
print(cameraUIView3)  CameraUI/ChromeSwiftUIView.swift:37: Fatal error: init(coder:) has not been implemented

I concluded that it’s a Swift class that likely has a Swift-only designated initializer (internal/private, not @objc) and the ObjC inits (-init, -initWithFrame:, -initWithCoder:) are intentionally trapped to fatalError.

  • Your ObjC-runtime dump only shows @objc methods. Swift-only initializers won’t appear there.
  • initWithCoder: is explicitly unimplemented, so nib/storyboard is out.
  • There is almost certainly a factory somewhere in CameraUI that returns this view.

❌ Invoking init methods of CameraUI.ChromeSwiftUIView via Objective-C runtime

My next approach was circumventing the traps by swizzling the init methods of the ChromeSwiftUIView with the init methods of it's superclass UIView:

// Replace -init with UIView's -init
let initSel = NSSelectorFromString("init")
let initMethod = try XCTUnwrap(class_getInstanceMethod(UIView.self, initSel))
let initImp = method_getImplementation(initMethod)
let initTypes = method_getTypeEncoding(initMethod)
class_replaceMethod(cls, initSel, initImp, initTypes)
defer {
    // TODO: revert the swizzling
}

// Replace -initWithFrame: with UIView's -initWithFrame:
let initWithFrameSel = NSSelectorFromString("initWithFrame:")
let initWithFrameMethod = try XCTUnwrap(class_getInstanceMethod(UIView.self, initWithFrameSel))
let initWithFrameImp = method_getImplementation(initWithFrameMethod)
let initWithFrameTypes = method_getTypeEncoding(initWithFrameMethod)
class_replaceMethod(cls, initWithFrameSel, initWithFrameImp, initWithFrameTypes)
defer {
    // TODO: revert the swizzling
}

let cameraUIView = try XCTUnwrap((cls as? UIView.Type)?.init(frame: .zero))
print(cameraUIView) // ✅ <CameraUI.ChromeSwiftUIView: 0x103c2ce90; frame = (0 0; 0 0); layer = <CALayer: 0x600000cf4b70>>

✅ Swizzling the type initializer

As the solution above is replacing class type methods which can have side effects, I also explored the option of raw object initialization using the Objective-C runtime

// 1) Allocate object memory without calling subclass init
let cameraView = try XCTUnwrap(class_createInstance(cls, 0) as? UIView)

// 2) Reinitialize storage using UIView’s initWithFrame:
typealias InitWithFrame = @convention(c) (AnyObject, Selector, CGRect) -> AnyObject
let sel = NSSelectorFromString("initWithFrame:")
let m = try XCTUnwrap(class_getInstanceMethod(UIView.self, sel))
let f = unsafeBitCast(method_getImplementation(m), to: InitWithFrame.self)
_ = f(cameraView, sel, .zero)

print(cameraView) // ✅ <CameraUI.ChromeSwiftUIView: 0x106e05270; frame = (0 0; 0 0); layer = <CALayer: 0x600000cf5110>>

✅ Swizzling the instance initializer

This implementation still relies on Objective-C, but I wanted to check if I can directly call the private Swift initializer too. I started to look into the LLVM symbol table dumper of the CameraUI framework to see if I can find any

$ export FRAMEWORK="/Library/Developer/CoreSimulator/Volumes/iOS_23A5326a/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 26.0.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/CameraUI.framework/CameraUI"
$ xcrun nm -m "$FRAMEWORK" | grep ChromeSwiftUIView
0000000000260cb8 (__TEXT,__text) non-external -[_TtC8CameraUI17ChromeSwiftUIView .cxx_destruct]
0000000000260474 (__TEXT,__text) non-external -[_TtC8CameraUI17ChromeSwiftUIView hitTest:withEvent:]
000000000025fe64 (__TEXT,__text) non-external -[_TtC8CameraUI17ChromeSwiftUIView initWithCoder:]
0000000000260c5c (__TEXT,__text) non-external -[_TtC8CameraUI17ChromeSwiftUIView initWithFrame:]
0000000000260140 (__TEXT,__text) non-external -[_TtC8CameraUI17ChromeSwiftUIView layoutSubviews]
00000000002604dc (__TEXT,__text) non-external -[_TtC8CameraUI17ChromeSwiftUIView pointInside:withEvent:]
000000000059e9f0 (__DATA,__objc_data) non-external (was a private external) _OBJC_CLASS_$__TtC8CameraUI17ChromeSwiftUIView
00100000005a8a48 (__TEXT,__text) non-external _OBJC_IVAR_$__TtC8CameraUI17ChromeSwiftUIView.scenePhaseView
00100000005a8a40 (__TEXT,__text) non-external _OBJC_IVAR_$__TtC8CameraUI17ChromeSwiftUIView.viewModel
00000000005a8a50 (__DATA,__data) non-external (was a private external) _OBJC_METACLASS_$__TtC8CameraUI17ChromeSwiftUIView
000000000059e990 (__DATA,__objc_data) non-external __DATA__TtC8CameraUI17ChromeSwiftUIView
0000000000327fa8 (__TEXT,__objc_methlist) non-external __INSTANCE_METHODS__TtC8CameraUI17ChromeSwiftUIView
000000000056c8c0 (__DATA_CONST,__objc_const) non-external __IVARS__TtC8CameraUI17ChromeSwiftUIView
000000000056c878 (__DATA_CONST,__objc_const) non-external __METACLASS_DATA__TtC8CameraUI17ChromeSwiftUIView
00000000003b46ce (__TEXT,__swift5_typeref) non-external (was a private external) _symbolic _____ 8CameraUI17ChromeSwiftUIViewC
00000000003cbd96 (__TEXT,__swift5_typeref) non-external (was a private external) _symbolic _____Sg 8CameraUI17ChromeSwiftUIViewC

To summarize this output, the available symbols only include the _TtC8CameraUI17ChromeSwiftUIView demangles to CameraUI.ChromeSwiftUIView (Swift class bridged to ObjC) and class and metaclass exist in the ObjC runtime (so NSClassFromString works), but symbol lookup by name ((lldb) image lookup -rn ChromeSwiftUIView) finds nothing because the relevant symbols aren’t exported.

❌ This means we can not dlsym a Swift initializer to invoke it directly and instance swizzling is the best approach.

Copy link

codecov bot commented Sep 2, 2025

❌ 9 Tests Failed:

Tests completed Failed Passed Skipped
3963 9 3954 10
View the top 3 failed test(s) by shortest run time
iOS_Swift_UITests.SessionReplayUITests::testCameraUI_shouldNotCrashOnIOS26
Stack Traces | 0s run time
.../iOS-Swift/iOS-Swift-UITests/SessionReplayUITests.swift:18 - XCTAssertTrue failed
iOS_Swift_UITests.ViewLifecycleUITests::testViewLifecycle_callingDismissWithLoadView_shouldNotCrashSDK
Stack Traces | 0s run time
.../iOS-Swift/iOS-Swift-UITests/ViewLifecycleUITests.swift:30 - Failed to tap "view-lifecycle-test" Button: Find single matching element. Multiple matching elements found for <XCUIElementQuery: 0x600002104730>.
iOS_Swift_UITests.ViewLifecycleUITests::testViewLifecycle_callingDismissWithViewDidAppear_shouldNotCrashSDK
Stack Traces | 0s run time
.../iOS-Swift/iOS-Swift-UITests/ViewLifecycleUITests.swift:75 - Failed to tap "view-lifecycle-test" Button: Find single matching element. Multiple matching elements found for <XCUIElementQuery: 0x600002139900>.
iOS_Swift_UITests.ViewLifecycleUITests::testViewLifecycle_callingDismissWithViewDidDisappear_shouldNotCrashSDK
Stack Traces | 0s run time
.../iOS-Swift/iOS-Swift-UITests/ViewLifecycleUITests.swift:105 - Failed to tap "view-lifecycle-test" Button: Find single matching element. Multiple matching elements found for <XCUIElementQuery: 0x600002148140>.
iOS_Swift_UITests.ViewLifecycleUITests::testViewLifecycle_callingDismissWithViewDidLayoutSubviews_shouldNotCrashSDK
Stack Traces | 0s run time
.../iOS-Swift/iOS-Swift-UITests/ViewLifecycleUITests.swift:135 - Failed to tap "view-lifecycle-test" Button: Find single matching element. Multiple matching elements found for <XCUIElementQuery: 0x600002140190>.
iOS_Swift_UITests.ViewLifecycleUITests::testViewLifecycle_callingDismissWithViewDidLoad_shouldNotCrashSDK
Stack Traces | 0s run time
.../iOS-Swift/iOS-Swift-UITests/ViewLifecycleUITests.swift:45 - Failed to tap "view-lifecycle-test" Button: Find single matching element. Multiple matching elements found for <XCUIElementQuery: 0x600002145090>.
iOS_Swift_UITests.ViewLifecycleUITests::testViewLifecycle_callingDismissWithViewWillAppear_shouldNotCrashSDK
Stack Traces | 0s run time
.../iOS-Swift/iOS-Swift-UITests/ViewLifecycleUITests.swift:60 - Failed to tap "view-lifecycle-test" Button: Find single matching element. Multiple matching elements found for <XCUIElementQuery: 0x6000021452c0>.
iOS_Swift_UITests.ViewLifecycleUITests::testViewLifecycle_callingDismissWithViewWillDisappear_shouldNotCrashSDK
Stack Traces | 0s run time
.../iOS-Swift/iOS-Swift-UITests/ViewLifecycleUITests.swift:90 - Failed to tap "view-lifecycle-test" Button: Find single matching element. Multiple matching elements found for <XCUIElementQuery: 0x600002147430>.
iOS_Swift_UITests.ViewLifecycleUITests::testViewLifecycle_callingDismissWithViewWillLayoutSubviews_shouldNotCrashSDK
Stack Traces | 0s run time
.../iOS-Swift/iOS-Swift-UITests/ViewLifecycleUITests.swift:120 - Failed to tap "view-lifecycle-test" Button: Find single matching element. Multiple matching elements found for <XCUIElementQuery: 0x600002150190>.

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copy link
Contributor

github-actions bot commented Sep 2, 2025

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1228.73 ms 1248.80 ms 20.06 ms
Size 23.75 KiB 969.25 KiB 945.50 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
9389467 1218.62 ms 1244.86 ms 26.24 ms
5d238d3 1228.94 ms 1253.04 ms 24.10 ms
bc0a04c 1226.83 ms 1255.04 ms 28.21 ms
67e8e3e 1220.08 ms 1229.23 ms 9.15 ms
3ec47ae 1231.02 ms 1256.67 ms 25.65 ms
f0e2579 1224.82 ms 1245.49 ms 20.67 ms
8047b99 1226.37 ms 1246.63 ms 20.26 ms
701b301 1226.10 ms 1245.57 ms 19.47 ms
07d7e83 1211.71 ms 1240.08 ms 28.37 ms
570f725 1206.00 ms 1238.96 ms 32.96 ms

App size

Revision Plain With Sentry Diff
9389467 23.75 KiB 866.51 KiB 842.76 KiB
5d238d3 23.75 KiB 913.62 KiB 889.88 KiB
bc0a04c 23.75 KiB 933.32 KiB 909.57 KiB
67e8e3e 23.75 KiB 919.91 KiB 896.16 KiB
3ec47ae 23.75 KiB 919.88 KiB 896.13 KiB
f0e2579 23.75 KiB 969.22 KiB 945.47 KiB
8047b99 23.75 KiB 855.37 KiB 831.62 KiB
701b301 23.75 KiB 867.16 KiB 843.41 KiB
07d7e83 23.75 KiB 913.27 KiB 889.52 KiB
570f725 23.74 KiB 913.38 KiB 889.63 KiB

Previous results on branch: issue-5647

Startup times

Revision Plain With Sentry Diff
d1b6626 1242.37 ms 1262.79 ms 20.42 ms
384aab3 1232.67 ms 1260.67 ms 28.00 ms
697c280 1206.27 ms 1231.76 ms 25.50 ms
f8f9ada 1214.33 ms 1249.85 ms 35.52 ms
9793ab3 1231.33 ms 1248.90 ms 17.57 ms
29dc424 1233.29 ms 1257.29 ms 24.01 ms
afcf7f2 1200.98 ms 1234.06 ms 33.08 ms

App size

Revision Plain With Sentry Diff
d1b6626 23.75 KiB 969.24 KiB 945.49 KiB
384aab3 23.75 KiB 963.05 KiB 939.30 KiB
697c280 23.75 KiB 969.24 KiB 945.49 KiB
f8f9ada 23.75 KiB 938.51 KiB 914.76 KiB
9793ab3 23.75 KiB 963.10 KiB 939.35 KiB
29dc424 23.75 KiB 969.23 KiB 945.48 KiB
afcf7f2 23.75 KiB 933.31 KiB 909.56 KiB

@philprime philprime self-assigned this Sep 2, 2025
@philprime
Copy link
Member Author

Using NSClassFromString will load the type into the Objective-C runtime and call it's +initialize() method, which could cause side-effects. Instead I will refactor it to convert the type of the current view and compare by their string name instead.

@philprime philprime marked this pull request as ready for review September 9, 2025 11:50
@philprime
Copy link
Member Author

@cursor review

cursor[bot]

This comment was marked as outdated.

The mapRedactRegion method could create duplicate redaction regions for views
that met both general redaction criteria and isViewSubtreeIgnored condition.
This happened when clipsToBounds was false, allowing both code paths to add
regions for the same view.

Fixed by moving the isViewSubtreeIgnored check to the beginning with an early
return, ensuring views are processed only once. Added comprehensive tests to
verify deduplication works correctly while maintaining proper redaction behavior."
@philprime
Copy link
Member Author

@philipphofmann we can wait with this PR until after #6075 so we have verification in CI that the tests work as expected.

Copy link
Member

@philipphofmann philipphofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for tackling this complex crash. I have a few questions.

@philprime
Copy link
Member Author

@philipphofmann is the new UI test failing on your machine too? It passes on my machine but fails in CI.

cursor[bot]

This comment was marked as outdated.

@philipphofmann
Copy link
Member

@philipphofmann is the new UI test failing on your machine too? It passes on my machine but fails in CI.

It runs successfully on my machine. I tested it with Xcode 26 RC and iPhone 17 Pro iOS 26 simulator.

Copy link
Member

@philipphofmann philipphofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@philprime philprime enabled auto-merge (squash) September 10, 2025 13:52
@philprime philprime merged commit 5840d2d into main Sep 10, 2025
171 of 176 checks passed
@philprime philprime deleted the issue-5647 branch September 10, 2025 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SessionReplay crashes when launching the camera in an app in iOS 26

2 participants