Skip to content

RUM / Session Replay crash and broad RUM tracking degradation after secure UITextField canvas screenshot protection #2898

@dineshiOSDev

Description

@dineshiOSDev

Stack trace

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2)

    frame #0:  CoreText`TBaseFont::GetInitializedGraphicsFont() const
    frame #1:  CoreText`TFont::GetAdvancesForGlyphsWithStyleFromCG(...)
    frame #2:  CoreText`TFont::GetUnsummedAdvancesForGlyphs(...)
    frame #3:  CoreText`CTFontGetTransformedAdvancesForGlyphsAndStyle
    frame #4:  UIFoundation`-[NSCoreTypesetter _NSFastDrawString:...]
    frame #5:  UIFoundation`-[NSCoreTypesetter _stringDrawingCoreTextEngineWithOriginalString:...]
    frame #6:  UIFoundation`__NSStringDrawingEngine
    frame #7:  UIFoundation`-[NSString(NSExtendedStringDrawing) boundingRectWithSize:options:attributes:context:]
    frame #8:  UIFoundation`-[NSString(NSStringDrawing) sizeWithAttributes:]
    frame #9:  UIKitCore`-[NSString(UIStringDrawingLegacy) _legacy_sizeWithFont:]
    frame #10: UIKitCore`-[UITextField _textSizeUsingFullFontSize:]
    frame #11: UIKitCore`-[UITextField _intrinsicSizeWithinSize:]
    frame #12: UIKitCore`-[UIView(UIConstraintBasedLayout) intrinsicContentSize]

  * frame #13: <AppBinary>.debug.dylib`SessionReplayViewAttributes.init(view:frame:clip:overrides:)
              at ViewTreeSnapshot.swift:135:42

    frame #14: <AppBinary>.debug.dylib`ViewTreeRecorder.recordRecursively(nodes:view:typeIndex:context:overrides:)
              at ViewTreeRecorder.swift:84:26

    frame #15-416:
              <AppBinary>.debug.dylib`ViewTreeRecorder.recordRecursively(nodes:view:typeIndex:context:overrides:)
              repeatedly recurses between the same small set of views
              at ViewTreeRecorder.swift:102:17

    frame #417: <AppBinary>.debug.dylib`ViewTreeRecorder.record(anyView:context:)
              at ViewTreeRecorder.swift:32:9

    frame #418: <AppBinary>.debug.dylib`UITextFieldRecorder.recordAppearance(textField:textFieldAttributes:context:)
              at UITextFieldRecorder.swift:67:32

    frame #419: <AppBinary>.debug.dylib`UITextFieldRecorder.semantics(view:attributes:context:)
              at UITextFieldRecorder.swift:49:31

    frame #421: <AppBinary>.debug.dylib`ViewTreeRecorder.nodeSemantics(view:attributes:context:)
              at ViewTreeRecorder.swift:130:52

    frame #422: <AppBinary>.debug.dylib`ViewTreeRecorder.recordRecursively(nodes:view:typeIndex:context:overrides:)
              at ViewTreeRecorder.swift:85:25

    frame #423: <AppBinary>.debug.dylib`ViewTreeRecorder.recordRecursively(nodes:view:typeIndex:context:overrides:)
              at ViewTreeRecorder.swift:102:17

    frame #424: <AppBinary>.debug.dylib`ViewTreeRecorder.record(anyView:context:)
              at ViewTreeRecorder.swift:32:9

    frame #425: <AppBinary>.debug.dylib`ViewTreeSnapshotBuilder.createSnapshot(rootView:recorderContext:)
              at ViewTreeSnapshotBuilder.swift:46:38

    frame #426: <AppBinary>.debug.dylib`WindowViewTreeSnapshotProducer.takeSnapshot(context:)
              at WindowViewTreeSnapshotProducer.swift:22:32

    frame #428: <AppBinary>.debug.dylib`Recorder.captureNextRecord(recorderContext:)
              at Recorder.swift:130:67

    frame #430: <AppBinary>.debug.dylib`closure #1 in RecordingCoordinator.captureNextRecord()
              at RecordingCoordinator.swift:166:45

    frame #432: <AppBinary>.debug.dylib`closure #1 in objc_rethrow<A>(_:file:line:)
              at DDError.swift:115:29

    frame #435: <AppBinary>.debug.dylib`+[__dd_private_ObjcExceptionHandler catchException:error:]
              at ObjcExceptionHandler.m:14:9

    frame #437: <AppBinary>.debug.dylib`objc_rethrow<A>(_:file:line:)
              at DDError.swift:113:27

    frame #438: <AppBinary>.debug.dylib`RecordingCoordinator.captureNextRecord()
              at RecordingCoordinator.swift:166:17

    frame #439: <AppBinary>.debug.dylib`closure #1 in RecordingCoordinator.init()
              at RecordingCoordinator.swift:64:51

    frame #441: <AppBinary>.debug.dylib`closure #1 in ScreenChangeScheduler.screenDidChange()
              at ScreenChangeScheduler.swift:80:30

    frame #443: <AppBinary>.debug.dylib`ScreenChangeScheduler.screenDidChange(snapshot:)
              at ScreenChangeScheduler.swift:80:20

    frame #444: <AppBinary>.debug.dylib`closure #1 in closure #1 in ScreenChangeScheduler.start()
              at ScreenChangeScheduler.swift:57:27

    frame #445: <AppBinary>.debug.dylib`CALayerChangeAggregator.deliverPendingChanges(now:)
              at CALayerChangeAggregator.swift:128:13

    frame #446: <AppBinary>.debug.dylib`closure #1 in CALayerChangeAggregator.scheduleDelivery()
              at CALayerChangeAggregator.swift:112:18

    frame #448: libdispatch.dylib`_dispatch_client_callout
    frame #449: libdispatch.dylib`_dispatch_continuation_pop
    frame #450: libdispatch.dylib`_dispatch_source_latch_and_call
    frame #451: libdispatch.dylib`_dispatch_source_invoke
    frame #452: libdispatch.dylib`_dispatch_main_queue_drain.cold.6
    frame #453: libdispatch.dylib`_dispatch_main_queue_drain
    frame #454: libdispatch.dylib`_dispatch_main_queue_callback_4CF
    frame #455: CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
    frame #456: CoreFoundation`__CFRunLoopRun
    frame #457: CoreFoundation`_CFRunLoopRunSpecificWithOptions
    frame #458: GraphicsServices`GSEventRunModal
    frame #459: UIKitCore`-[UIApplication _run]
    frame #460: UIKitCore`UIApplicationMain
    frame #461: <AppBinary>.debug.dylib`main
    frame #462: dyld`start

We can provide the full symbolicated crash log privately if needed. The crash is reproducible locally when attached to Xcode after enabling a secure screenshot-protection layer. Please see the attached crashing thread stack trace.

Reproduction steps

Reproduction steps

  • Integrate Datadog iOS SDK with RUM enabled.
  • Enable automatic UIKit action tracking using DefaultUIKitRUMActionsPredicate.
  • Enable Session Replay.
  • Add screenshot protection using the common secure UITextField canvas technique:
  • create a secure UITextField
  • set isSecureTextEntry = true
  • find the private secure canvas view/layer
  • move/reparent the app UIWindow.layer under that secure canvas layer
  • Launch the app from Xcode in Debug.
  • Navigate through normal UIKit/SwiftUI screens.
  • Toggle the screenshot-protection feature on/off.
  • Observe that Datadog RUM tracking becomes unreliable:
  • most view tracking stops or becomes inconsistent
  • most tap/action tracking stops or becomes inconsistent
  • some automatic UIKit actions may still appear
  • In some debug/Xcode-attached runs, the app crashes inside or around Datadog SDK behavior.
  • Reproduced with Datadog iOS SDK 3.10.0.

Expected behavior

Datadog SDK should not crash or break broad RUM tracking when the app uses secure screenshot protection.

If the secure UITextField canvas technique is unsupported, we would like guidance on the recommended way to:

  • keep screenshot protection enabled
  • keep RUM view/action tracking working
  • avoid Session Replay crashes or invalid view hierarchy traversal
  • optionally disable or exclude Session Replay for protected content without breaking RUM
  • Actual behavior

After enabling the secure screenshot-protection implementation:

  • Datadog RUM view tracking becomes unreliable.
  • Datadog automatic action tracking becomes unreliable.
  • Manual action tracking for the screen-overlay toggle is also not reliably visible.
  • Some older automatic events still appear, for example generic UIKit tap events, but the new screen-overlay action is not consistently visible.
  • In Debug while attached to Xcode, the app can crash.
  • The same flow appears more stable when launched without Xcode attached.
  • Screenshot-protection implementation shape

The implementation uses a secure UITextField as a screenshot-protection container. The app locates the secure text field canvas view/layer and reparents the app window layer under that secure canvas layer so screenshots are blanked by iOS.

Pseudo-code:

let textField = UITextField()
textField.isSecureTextEntry = true

window.addSubview(textField)

let canvas = textField.subviews.first {
    String(describing: type(of: $0)).contains("Canvas")
}

window.layer.superlayer?.addSublayer(textField.layer)
canvas?.layer.addSublayer(window.layer)

The app later removes the secure layer and restores the original window layer position when protection is disabled.

Datadog setup

Datadog.initialize(
    with: Datadog.Configuration(
        clientToken: "<redacted>",
        env: "<env>",
        service: "<service>"
    ),
    trackingConsent: .granted
)

RUM.enable(
    with: RUM.Configuration(
        applicationID: "<redacted>",
        uiKitViewsPredicate: DefaultUIKitRUMViewsPredicate(),
        uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate()
    )
)

SessionReplay.enable(
    with: SessionReplay.Configuration(
        replaySampleRate: <sample-rate>,
        textAndInputPrivacyLevel: .maskSensitiveInputs,
        imagePrivacyLevel: .maskNone,
        touchPrivacyLevel: .show
    )
)

Volume

Local/debug reproduction so far. The crash is reproducible when running from Xcode after enabling screenshot protection and interacting with the app.

Not released to production yet. This issue was found during pre-release validation of an upcoming screenshot-protection feature. No production users are currently affected.

Affected SDK versions

3.10.0

We also observed similar behavior while validating older SDK versions 3.7.0 and 3.9.1, but the attached stack trace is from 3.10.0.

Latest working SDK version

We have not identified a Datadog SDK version where this secure screenshot-protection flow works correctly. Tested with 3.10.0; the issue still reproduces.

Does the crash manifest in the latest SDK version?

Yes

Deployment Target

iOS 17+

Device Information

iOS versions:

Tried on both latest 26.4.2 and older version 18.3

Other relevant information

We are implementing product-required screenshot protection. Removing the secure screenshot-protection layer is not currently an option.

The issue appears related to Datadog inspecting/traversing or swizzling UIKit/SwiftUI view hierarchy while the app window layer has been moved under a secure UITextField canvas. We need guidance on whether this setup is unsupported and, if so, what SDK-supported approach is recommended for apps that must protect screenshots while still using RUM and Session Replay.

We also observed that not only the new screen-overlay action is missing. Most view and action tracking becomes unreliable after this implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    awaiting responseWaiting for response / confirmation from the reportercrashSDK crashes

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions