{"id":111560,"date":"2025-09-05T07:00:00","date_gmt":"2025-09-05T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=111560"},"modified":"2025-09-08T08:46:22","modified_gmt":"2025-09-08T15:46:22","slug":"20250905-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20250905-00\/?p=111560","title":{"rendered":"The case of the crash on a null pointer even though we checked it for null"},"content":{"rendered":"<p>A colleague was investigating a crash. The stack at the point of the crash looks like this:<\/p>\n<pre>contoso!winrt::impl::consume_Windows_Foundation_Collections_IVectorView&lt;\r\n            winrt::Windows::Foundation::Collections::IVectorView&lt;int&gt;,\r\n            int&gt;::Size+0x30\r\ncontoso!winrt::Contoso::implementation::Widget::\r\n            InitializeNodesAsync$_ResumeCoro$1+0x2bc\r\ncontoso!wil::details::coro::apartment_resumer::resume_apartment_callback+0x28\r\ncombase!CRemoteUnknown::DoCallback+0x34\r\ncombase!CRemoteUnknown::DoNonreentrantCallback+0x48\r\nrpcrt4!Invoke+0x64\r\nrpcrt4!InvokeHelper+0x130\r\nrpcrt4!Ndr64StubWorker+0x6cc\r\nrpcrt4!NdrStubCall3+0xdc\r\ncombase!CStdStubBuffer_Invoke+0x6c\r\ncombase!ObjectMethodExceptionHandlingAction&lt;\u27e6...\u27e7&gt;+0x48\r\ncombase!DefaultStubInvoke+0x2b8\r\ncombase!SyncServerCall::StubInvoke+0x40\r\ncombase!StubInvoke+0x170\r\ncombase!ServerCall::ContextInvoke+0x3c4\r\ncombase!ReentrantSTAInvokeInApartment+0x1fc\r\ncombase!ComInvokeWithLockAndIPID+0xcc4\r\ncombase!ThreadDispatch+0x514\r\ncombase!ThreadWndProc+0x1b4\r\nuser32!UserCallWinProcCheckWow+0x180\r\nuser32!DispatchMessageWorker+0x130\r\n<\/pre>\n<p>If we look at the point of the fault:<\/p>\n<pre>0:001&gt;  r\r\nLast set context:\r\n x0=0000000000000000   x1=00000053af4fdf78   x2=0000017b4f7e6380   x3=0000000000000000\r\n x4=6597e92abf947185   x5=857194bf2ae99765   x6=857194bf2ae99765   x7=0000017b4f700000\r\n x8=00007ff85b11ce40   x9=0000000000000001  x10=0000006700000000  x11=0000000000000000\r\nx12=fffffffffff00000  x13=0000000000000000  x14=0000000000000000  x15=000000000000000b\r\nx16=00007ff8d4fd44a0  x17=ffff68553f08a1c6  x18=00007ff8d0bc0000  x19=0000017b4c834de0\r\nx20=0000000000000000  x21=00007ff8d45cd188  x22=00007ff8d45cd188  x23=00007ff8d45cd150\r\nx24=0000017b31196930  x25=00000053af4fdfa0  x26=00000053af4fe580  x27=0000000000000010\r\nx28=0000000000000000   fp=00000053af4fdf90   lr=00007ff85af448cc   sp=00000053af4fdf60\r\n pc=00007ff85af448f0  psr=80001040 N--- EL0\r\ncontoso!winrt::impl::consume_Windows_Foundation_Collections_IVectorView&lt;\r\n    winrt::Windows::Foundation::Collections::IVectorView&lt;int&gt;,\r\n    int&gt;::Size+0x30:\r\n00007ff8`5af448f0 f9400008 ldr         x8,[x0]\r\n<\/pre>\n<p>we see that we are crashing on a null pointer (<tt>x0<\/tt>).<\/p>\n<p>The function is a C++\/WinRT <code>consume_<\/code>, which is just a projection of the underlying COM call. The COM call is performed by reading the vtable pointer from the object, reading the function pointer from the vtable, and then calling the function.<\/p>\n<p>The fact that we are reading from <tt>x0<\/tt> (the inbound and outbound parameter slot) means that this is almost certainly reading the vtable pointer from the object: We want the COM pointer in <tt>x0<\/tt> for the outbound call, so the obvious thing to do is to leave it there while you read the vtable pointer from it.<\/p>\n<p>We can confirm this by reading the disassembly.<\/p>\n<pre>0:001&gt; u .-30 .\r\ncontoso!winrt::impl::consume_Windows_Foundation_Collections_IVectorView&lt;\r\n    winrt::Windows::Foundation::Collections::IVectorView&lt;int&gt;,\r\n    int&gt;::Size+0x30:\r\n00007ff8`5af448c0 stp         fp,lr,[sp,#-0x10]!   ; build stack frame\r\n00007ff8`5af448c4 mov         fp,sp\r\n00007ff8`5af448c8 bl          contoso!__security_push_cookie (00007ff8`5ab91050)\r\n00007ff8`5af448cc sub         sp,sp,#0x20\r\n00007ff8`5af448d0 mov         w8,#0x1E4\r\n00007ff8`5af448d4 <span style=\"border: solid 1px currentcolor;\">ldr         x0,[x0]              ; fetch the COM pointer<\/span>\r\n00007ff8`5af448d8 str         wzr,[sp,#0x18]\r\n00007ff8`5af448dc str         w8,[sp]\r\n00007ff8`5af448e0 adrp        x8,contoso!`string'+0x10 (00007ff8`5b11c000)\r\n00007ff8`5af448e4 add         x8,x8,#0xE40\r\n00007ff8`5af448e8 stp         x8,xzr,[sp,#8]\r\n00007ff8`5af448ec add         x1,sp,#0x18\r\n00007ff8`5af448f0 <span style=\"border: solid 1px currentcolor;\">ldr         x8,[x0]              ; read the vtable<\/span>\r\n<\/pre>\n<p>(The other code is recording the line number and file name for diagnostic purposes.)<\/p>\n<p>Okay, so we are calling <code>IVectorView&lt;T&gt;::Size<\/code> on a null pointer.<\/p>\n<p>Let&#8217;s see whose idea that is.<\/p>\n<p>Here&#8217;s the caller:<\/p>\n<pre>winrt::IAsyncAction Widget::InitializeNodesAsync()\r\n{\r\n    auto lifetime = get_strong();\r\n    std::optional&lt;winrt::IVectorView&lt;int32_t&gt;&gt; numbers;\r\n    co_await winrt::resume_background();\r\n    CallWithRetry([&amp;] {\r\n        numbers = GetMagicNumbers();\r\n    });\r\n\r\n    if (numbers == nullptr)\r\n    {\r\n        co_return;\r\n    }\r\n\r\n    co_await winrt::resume_foreground(m_uithread);\r\n\r\n    std::vector&lt;winrt::Node&gt; nodes;\r\n    nodes.reserve((*numbers).Size()); \/\/ \u2190 CRASH HERE\r\n<\/pre>\n<p>The crash inside <code>consume_<\/code> tells us that <code>(*numbers)<\/code> is null. Let&#8217;s see if we can confirm that in the debugger.<\/p>\n<p>First, we have to find <code>numbers<\/code>.<\/p>\n<pre>0:001&gt; dv \/V\r\n@x19              @x19        __coro_frame_ptr = 0x0000017b`4c834de0\r\n00000000`00000088 @x25+0x0590         lifetime = struct winrt::com_ptr&lt;\r\n                                                     winrt::Contoso::implementation::Widget&gt;\r\n00000000`000000e8 @x25+0x0590            nodes = class std::vector&lt;int&gt;\r\n00000000`000000d8 @x25+0x0590          numbers = class std::optional&lt;winrt::Windows::Foundation::\r\n                                                     Collections::IVectorView&lt;int&gt; &gt;\r\n\u27e6 ... \u27e7\r\n<\/pre>\n<p>The debugger says that <code>numbers<\/code> is at <code>@x25+0x0590<\/code>, and that this calculates out to <code>00000000`000000d8<\/code>, which is nonsense. So we can&#8217;t really trust that calculation.<\/p>\n<p>Let&#8217;s see what the code uses. We disassemble backward from the return address.<\/p>\n<pre>0:001&gt; k2\r\nChild-SP          RetAddr           Call Site\r\n00000053`af4fdf60 00007ff8`5b059c8c contoso!winrt::impl::consume_Windows_Foundation_Collections_IVectorView&lt;\r\n                                        winrt::Windows::Foundation::Collections::IVectorView&lt;int&gt;,\r\n                                        int&gt;::Size+0x30\r\n00000053`af4fdfa0 00007ff8`5abc5108 contoso!winrt::Contoso::implementation::Widget::\r\n                                        InitializeNodesAsync$_ResumeCoro$1+0x2bc\r\n<\/pre>\n<p>We read the return address from the function one deeper on the stack, giving us <code>00007ff8`5b059c8c<\/code>.<\/p>\n<pre>00007ff8`5b059c84 add  x0,x19,#0xD8 \/\/ \u2190 setting up the call to consume_\r\n00007ff8`5b059c88 bl   contoso!winrt::impl::consume_Windows_Foundation_Collections_IVectorView&lt;\r\n                           winrt::Windows::Foundation::Collections::IVectorView&lt;int&gt;,\r\n                           int&gt;::Size\r\n00007ff8`5b059c8c uxtw x1,w0\r\n<\/pre>\n<p>From the disassembly, we see that the compiler stored the <code>IVector<\/code> part of the <code>numbers<\/code> at offset <code>0xD8<\/code> from the coroutine frame, which is in <code>x19<\/code>.<\/p>\n<p>We can pluck the coroutine frame from the <code>dv<\/code> output, or we can ask the debugger to restore the nonvolatile registers for us (which includes <code>x19<\/code>):<\/p>\n<pre>0:001&gt; .frame \/c 2\r\n0:001&gt; dps @x19+0xd8\r\n0000017b4c834eb8  0000000000000000 \/\/ &lt;&lt;&lt;&lt;&lt; the stored IVector\r\n0000017b4c834ec0  0000017b4ff78400\r\n<\/pre>\n<p>We can ask the debugger for the layout of the <code>std::optional<\/code> so we can see the full <code>numbers<\/code>.<\/p>\n<p>I copied the type name from the earlier <code>dv<\/code> output.<\/p>\n<pre>0:001&gt; dt contoso!std::optional&lt;winrt::Windows::Foundation::Collections::IVectorView&lt;int&gt; &gt;\r\n   +0x000 _Dummy           : std::_Nontrivial_dummy_type\r\n   +0x000 _Value           : winrt::Windows::Foundation::Collections::IVectorView&lt;int&gt;\r\n   +0x008 _Has_value       : Bool\r\n<\/pre>\n<p>Okay, so the value is kept at offset zero, and the <code>_Has_value<\/code> is at offset 8.<\/p>\n<p>We can eyeball from the earlier <tt>dps<\/tt> command that the <code>_Value<\/code> is nullptr, and the <code>_Has_value<\/code> is false. (Little-endian means that the single <code>bool<\/code> is in the least significant byte of the 8-byte value.)<\/p>\n<p>Or we can ask the debugger to interpret it for us.<\/p>\n<pre>0:001&gt; dt contoso!std::optional&lt;winrt::Windows::Foundation::Collections::IVectorView&lt;int&gt; &gt; @x19+0xd8\r\n   +0x000 _Dummy           : std::_Nontrivial_dummy_type\r\n   +0x000 _Value           : winrt::Windows::Foundation::Collections::IVectorView&lt;int&gt;\r\n   +0x008 _Has_value       : 0\r\n<\/pre>\n<p>Okay, so the <code>numbers<\/code> has no value.<\/p>\n<p>But wait, our code checked for that!<\/p>\n<pre>        if (numbers == nullptr)\r\n        {\r\n            co_return;\r\n        }\r\n<\/pre>\n<p>Why didn&#8217;t that work?<\/p>\n<p>Because that&#8217;s not how <code>std::optional<\/code> works.<\/p>\n<p>The <code>std::optional<\/code> is a sum type of <code>T<\/code> with a special value called <code>std::nullopt<\/code>. If you compare a <code>std::optional<\/code> against anything that isn&#8217;t <code>std::nullopt<\/code>, then you are checking if the <code>std::optional<\/code> has a value that matches the value you are comparing against.<\/p>\n<table class=\"cp3\" style=\"border-collapse: collapse;\" border=\"1\" cellspacing=\"0\" cellpadding=\"3\">\n<tbody>\n<tr>\n<th rowspan=\"2\"><code>std::optional holds<\/code><\/th>\n<th colspan=\"2\">compared with<\/th>\n<\/tr>\n<tr>\n<th><code>std::nullopt<\/code><\/th>\n<th><code>Y<\/code><\/th>\n<\/tr>\n<tr>\n<td><code>std::nullopt<\/code><\/td>\n<td>true<\/td>\n<td>false<\/td>\n<\/tr>\n<tr>\n<td><code>X<\/code><\/td>\n<td>false<\/td>\n<td><code>X == Y<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>For the purposes of comparison, an empty <code>std::optional<\/code> is treated as if it had the value <code>std::nullopt<\/code>, which is a value distinct from the value values of <code>T<\/code>.<\/p>\n<p>Therefore, writing <code>if (numbers == nullptr)<\/code> means &#8220;if <code>numbers<\/code> has a value that is equal to <code>nullptr<\/code>&#8220;.<\/p>\n<p>But in this case, <code>numbers<\/code> has no value, so the comparison fails, and we fall through.<\/p>\n<p>Then we dereference the <code>*numbers<\/code>, which is specified to retrieve the wrapped value, and it is undefined behavior if the <code>numbers<\/code> has no value.<\/p>\n<p>In our case, the <code>numbers<\/code> indeed has no value, so we have entered the world of undefined behavior. In practice, what happens is that we read whatever is in <code>_Value<\/code>, and we saw in the debugger, that <code>_Value<\/code> holds a null pointer. We then try to call <code>Size()<\/code> on a null pointer and crash.<\/p>\n<p>One fix is to change the test from <code>if (numbers == nullptr)<\/code> to <code>if (!numbers.has_value())<\/code> to ask whether the <code>numbers<\/code> is empty.<\/p>\n<p>But this is working too hard.<\/p>\n<p>The use of <code>std::optional&lt;T&gt;<\/code> was itself unnecessary. There is already a natural empty value for <code>IVector<\/code>, namely <code>nullptr<\/code>. So we can declare <code>numbers<\/code> as an <code>IVector<\/code>, which default-initializes to <code>nullptr<\/code>, and then check whether it is still <code>nullptr<\/code> after we try (and possibly fail) to get a value.<\/p>\n<pre>winrt::IAsyncAction Widget::InitializeNodesAsync()\r\n{\r\n    auto lifetime = get_strong();\r\n    <span style=\"border: solid 1px currentcolor;\">winrt::IVectorView&lt;int32_t&gt;<\/span> numbers; \/\/ remove std::optional\r\n    co_await winrt::resume_background();\r\n    CallWithRetry([&amp;] {\r\n        numbers = GetMagicNumbers();\r\n    });\r\n\r\n    if (numbers == nullptr)\r\n    {\r\n        co_return;\r\n    }\r\n\r\n    co_await winrt::resume_foreground(m_uithread);\r\n\r\n    std::vector&lt;winrt::Node&gt; nodes;\r\n    nodes.reserve(<span style=\"border: solid 1px currentcolor;\">numbers<\/span>.Size()); \/\/ remove the *\r\n\r\n    \u27e6...\u27e7\r\n<\/pre>\n<p>This change also covers the case where <code>GetMagicNumbers<\/code> succeeds but returns a null pointer.<\/p>\n<p>In practice, <code>GetMagicNumbers<\/code> never returns a null pointer because it knows that <a title=\"Embracing the power of the empty set in API design (and applying this principle to selectors and filters)\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20240812-00\/?p=110121\"> the empty set is not the same as no set at all<\/a>. The original code was testing against something that never happens.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Understanding what you&#8217;re checking.<\/p>\n","protected":false},"author":1069,"featured_media":110434,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-111560","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Understanding what you&#8217;re checking.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111560","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=111560"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111560\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/110434"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=111560"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=111560"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=111560"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}