Skip to content

iOS: scrolling a UIStackView in a UIScrollView while mutating labels aborts with 'insertObject:atIndex:: object cannot be nil' #7763

Description

@proggeramlug

Summary

Scrolling a UIScrollView-hosted UIStackView on a physical device while JS mutates its labels aborts the app:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
    reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
App terminated due to signal 6.

Requires human touch input. No automated probe reproduces it, which is why it has gone unseen — every headless run passes.

Where it throws

The backtrace is start → main → UIApplicationMain → CFRunLoop → CA transaction commit → UIView layout → UIStackView → throw. The exception lands in the layout pass at the end of a run-loop iteration, not inside any Perry callback. Something earlier leaves a nil in a UIKit array; it detonates at commit time.

Reproduction

Device: iPhone 16 Pro, iOS 26, Release build, --target ios.

PERRY_NO_AUTO_OPTIMIZE=1 perry run ios benchmarks/ios-ui/bench.ts --device <UDID>

Scroll the list continuously during the property-updates phase (50 labels, textSetString on each every frame) or add-remove (100 rows rebuilt 20x/sec). Crash lands within a few seconds. Without scrolling, the same build runs indefinitely.

PERRY_NO_AUTO_OPTIMIZE=1 is needed because perry-stdlib currently fails to build with --no-default-features — separate issue, see below.

Not caused by the frame-metrics work (#7754)

Established by four single-variable device bisects, each with a human scrolling:

arm result
deferred view release crashes
widget release disabled entirely crashes
display link in NSDefaultRunLoopMode crashes
display-link driver disabled, original NSTimer pump crashes

The last arm is decisive: with #7754's driver removed and onFrame back on the pre-PR js_frame_pump_default() path, the abort still occurs. That PR's benchmark is simply the first workload that drives enough label mutation against a scrolled stack to expose it.

Hypotheses tested and falsified

  • Deallocating views inside CoreAnimation's pre-commit phase — falsified; crashes with release deferred to a quiescent point.
  • Releasing widget-table entries at all — falsified; crashes with release fully disabled. An isolation probe released ~323k widgets inline with no fault.
  • Running JS during UITrackingRunLoopMode — falsified; crashes with the link in default mode, where JS is frozen during the drag.

Suggested next step

Symbolicate a full crash report (the raw frames above have no load address), and audit every path that can hand a view to a UIStackView arranged-subview array. add_child / add_child_at guard their handle lookups, so the nil likely enters via a UIKit-held reference to a view that went away, or via constraint bookkeeping in HEIGHT_CONSTRAINTS.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions