dotnet / dotnet/maui

[iOS] NavigationRenderer.SetStatusBarStyle null check does not protect the following NavPage dereferences

Open Beginner friendly
#38,153 0 comments 1 reaction 0 assignees View on GitHub
area-controls-navigationpage platform/ios
Dominant language
C#
Stars
23.3k
Forks
2k
Avg merge
1d 15h
Merged PRs (30d)
290

Description

### Description

`NavigationRenderer.SetStatusBarStyle()` guards against a null `NavPage` and then dereferences it twice more. `NavPage` is a computed property backed by a weak reference, so the guard does not protect the two uses that follow — if the element is released or the handler disconnected in between, the method throws `NullReferenceException`.

https://github.com/dotnet/maui/blob/main/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs

```csharp
NavigationPage NavPage => Element as NavigationPage; // re-evaluated on every access
public VisualElement Element { get => _viewHandlerWrapper.Element ?? _element?.GetTargetOrDefault(); } // weak reference

void SetStatusBarStyle()
{
if (NavPage is null) // access 1 - passes
{
return;
}

var barTextColor = NavPage.BarTextColor; // access 2 - can be null here
var statusBarColorMode = NavPage.OnThisPlatform().GetStatusBarTextColorMode(); // access 3
...
}
```

Because `SetStatusBarStyle()` is called from `ViewWillAppear`, the exception escapes through the ObjC registrar trampoline to `UIApplicationMain` and terminates the process, rather than surfacing as a catchable managed exception:

```
System.NullReferenceException: Arg_NullReferenceException
at Microsoft.Maui.Controls.Handlers.Compatibility.NavigationRenderer.SetStatusBarStyle()
at Microsoft.Maui.Controls.Handlers.Compatibility.NavigationRenderer.ViewWillAppear(Boolean animated)
at ...__Registrar_Callbacks__.callback_..._ViewWillAppear(IntPtr pobj, IntPtr sel, Byte p0, IntPtr* exception_gchandle)
at ObjCRuntime.Runtime.ThrowException(IntPtr gchandle)
at UIKit.UIApplication.UIApplicationMain(...)
```

This is the same signature as #29535 and #29564. Those were closed after the `if (NavPage is null) return;` guard was added, but the guard cannot fix this pattern: it validates a value that is then discarded and re-read. We still see the crash on builds that contain the guard.

The comment on #29535 that "NET9 does cleanup code a bit more aggressively than NET8" is consistent with this — more eager teardown widens the window between the check and the uses.

### Steps to Reproduce

We have not been able to reproduce this on demand, which is consistent with it being a race. It reproduces in the field for us when a third-party popup library repeatedly changes which `UIWindow` is key: the library demotes the app window (`WindowLevel = -1`), presents its own window with `MakeKeyAndVisible()`, and reverses that on dismissal. Each cycle drives an extra `ViewWillAppear` on the `NavigationRenderer` while views are being torn down and re-shown.

Crash breadcrumbs consistently show the popup/navigation cycle immediately before the crash:

```
PopupPageRenderer -> NavigationRenderer_ParentingViewController
PopupPageRenderer -> NavigationRenderer_ParentingViewController
PopupPageRenderer -> NavigationRenderer_ParentingViewController <- crash here
```

Any scenario that disconnects the handler or releases the page concurrently with `ViewWillAppear` should be able to hit it.

### Link to public reproduction project repository

_No response_

### Version with bug

10.0.71, 10.0.90, 10.0.100 (the method is byte-identical across these and current `main`)

### Is this a regression from previous behavior?

Yes, this used to work in .NET MAUI

### Last version that worked well

Unknown/Other

### Affected platforms

iOS

### Affected platform versions

iOS 26.x, seen across iPhone 12 through iPhone 16 Plus

### Did you find any workaround?

No workaround from application code — the race is entirely inside `SetStatusBarStyle()`. Avoiding whatever provokes the extra `ViewWillAppear` cycles reduces the frequency but does not address the cause.

### Relevant log output

```shell
System.NullReferenceException: Arg_NullReferenceException (System.NullReferenceException)
at Microsoft.Maui.Controls.Handlers.Compatibility.NavigationRenderer.SetStatusBarStyle()
at Microsoft.Maui.Controls.Handlers.Compatibility.NavigationRenderer.ViewWillAppear(Boolean animated)
at Microsoft.Maui.Controls.Handlers.Compatibility.NavigationRenderer.__Registrar_Callbacks__.callback_795_Microsoft_Maui_Controls_Handlers_Compatibility_NavigationRenderer_ViewWillAppear(IntPtr pobj, IntPtr sel, Byte p0, IntPtr* exception_gchandle)

at __exceptionPreprocess
at objc_exception_throw
at log_callback(char const*, char const*, char const*, int, void*) (runtime.m:1073)
at -[Microsoft_Maui_Controls_Handlers_Compatibility_NavigationRenderer viewWillAppear:] (registrar.mm:20168)
at -[UIViewController _setViewAppearState:isAnimating:]
at -[UIViewController __viewWillAppear:]
```

### Proposed fix

Read `NavPage` once into a local and use that, so the guard actually protects the subsequent uses:

```csharp
void SetStatusBarStyle()
{
var navPage = NavPage;

if (navPage is null)
{
return;
}

var barTextColor = navPage.BarTextColor;
var statusBarColorMode = navPage.OnThisPlatform().GetStatusBarTextColorMode();

// ... unchanged
}
```

This is behaviour-preserving when the element is alive and removes the crash when it is not.

It is worth noting that `NavPage` is dereferenced after a null check in several other places in this file (for example around the `Element == null` checks). The same pattern may be worth auditing, since every one of them has the same property-versus-local hazard.

Contributor guide

Open the contributing guide

Research direction

Open src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs and trace ViewWillAppear into SetStatusBarStyle. Verify NavPage is read once before the null check and that the later accesses use the guarded value; also review the other NavPage checks noted in the file. Done when the race-prone dereferences are eliminated without changing the remaining behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
mobile
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.