Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does excel crash here?

Is there any reason the below crashes excel 2016?

Option Explicit
Option Compare Text
Option Base 1

Sub p1()
    Dim t(1), w()
    t(1) = w
    If IsMissing(w) Then DoEvents
    If IsMissing(t(1)) Then DoEvents
End Sub

w is an uninitiated array, but passing w into ismissing(w) is fine, yet when passing it via the t(1), excel reproducibly terminates ungracefully...

This is the shortest bit of code I have managed to generate using vba only that always kills the excel application (i.e. not using external code like createobject or shell).

Are there other such examples?

cheers

like image 509
vbAdder Avatar asked Oct 07 '18 00:10

vbAdder


People also ask

Why Microsoft Excel keeps crashing?

Microsoft Excel may crash due to any one or more reasons given below, Incompatible Add-Ins. Outdated MS Excel program. Conflict with other programs or antivirus tool.

Why is Excel opening and then closing?

If your Excel opens and closes immediately then the following are the responsible causes behind this: Outdated MS Excel application. Network connection Problem. Incompatible Add-Ins.


1 Answers

tl;dr

You confused the hell out of IsMissing by making it think an uninitialized array was a malformed ParamArray, and hit a bug in its implementation.


According to the Windows event viewer, the underlying exception that brings down Excel is an access violation in vbe7.dll:

The first report in I saw (commented on above) was a Rubberduck callback failing because Excel was toast.

Faulting application name: EXCEL.EXE, version: 15.0.5067.1000, time stamp: 0x5b76360d
Faulting module name: VBE7.DLL, version: 7.1.10.68, time stamp: 0x58def301
Exception code: 0xc0000005
Fault offset: 0x00000000001cd38c
Faulting process id: 0xac4
Faulting application start time: 0x01d45de81ef072ed
Faulting application path: C:\Program Files\Microsoft Office\Office15\EXCEL.EXE
Faulting module path: C:\PROGRA~1\COMMON~1\MICROS~1\VBA\VBA7.1\VBE7.DLL

The access violation is due to the VB run-time attempting to dereference a null pointer. This is the where the debugger lands you as it dies:

00007FFF9CDDD383  mov         rax,qword ptr [rsp+30h]  
00007FFF9CDDD388  mov         rax,qword ptr [rax+8]  
00007FFF9CDDD38C  movzx       eax,word ptr [rax]    <--fails, rax is 0.
00007FFF9CDDD38F  cmp         eax,1  
00007FFF9CDDD392  jne         00007FFF9CD7B8A4

So, let's take a look at what the IsMissing function is checking for. VBA is basically a beast of COM, and an optional parameter that isn't passed is received by the called function as a Variant with a type of VT_ERROR per RPC convention. It isn't passed as Null, Empty, or any other VB construct. The following code demonstrates:

Sub Foo()
    Bar
End Sub

Sub Bar(Optional x)
    Debug.Print VarPtr(x)   'This has a valid pointer.
    Debug.Print IsError(x)  'True
    Debug.Print VarType(x)  '10 (vbError)
End Sub

You can read a slightly more technical explanation on Raymond Chen's blog.


IsMissing is intended to be passed an incoming parameter, so it also has to be aware of ParamArrays (under the hood, a ParamArray is just a Variant()). Although, if you don't give any parameters to it the procedure still gets an array - it just has a UBound lower than the LBound:

Sub Calling()
    Called
End Sub

Sub Called(ParamArray params())
    Debug.Print LBound(params)   '<-- 0
    Debug.Print UBound(params)   '<-- -1
End Sub

Here's where it gets interesting. You can't have both a ParamArray and an optional argument in the same procedure signature - it can be one or the other. When you look at the function signature, you'll see that it takes a pointer to a Variant (that is, it expects a ByRef parameter):

[entry(0x60000007), helpcontext(0x000f69b5)]
VARIANT_BOOL _stdcall IsMissing([in] VARIANT* ArgName);

But code like this doesn't give the function what it's expecting to see:

Sub DieExcelDie()
    Dim x(1), y()
    x(1) = y
    x(1) = IsMissing(x(1))
End Sub

x(1) has to be marshalled into a ByRef Variant to be passed as a parameter to IsMissing. So what it sees is an array of Variant that has both an LBound and UBound value of zero (this answer goes a bit into what an uninitialized array looks like in memory). The parameter has not already gone through the RPC mechanism, so a this point it is completely indistinguishable from a ParamArray containing one passed parameter. It would check to see if the variant was typed as VT_ERROR, see that it was an array, and attempt to process it that way. Since it doesn't comply with the spec for an empty parameter list, it likely checks to see what the first passed parameter was... by dereferencing the data area. The SAFEARRAY structure looks like this1:

SAFEARRAY in memory

Going back to the disassembly and without going completely down the rabbit hole of disassembling vbe7.dll, the rax+8 on the previous instruction is most likely loading up the pvData pointer (it's 8 bytes on a 64 bit version of Excel), which you can see is zero.


This is simply a bug in the implementation of IsMissing - it should be checking for a null pointer instead of blindly dereferencing it. I suspect, although can't verify, that it was introduced in VB5 along with the changes that allowed both non-Variant optional parameters and default values.


1Image was taken as a screenshot from How Visual Basic 6 Stores Data on codeguru.

like image 178
Comintern Avatar answered Sep 22 '22 23:09

Comintern