Repository navigation
Division by zero in Curl_now #3572
Description
I did this
Execute unit tests for our application, which uses libcurl. Sometimes we see a crash in Curl_now with STATUS_INTEGER_OVERFLOW at the count.QuadPart / freq.QuadPart division. It's not every time, but with two of our tests we see one crash every 10 ~ 100 runs. So not really that rare.
I expected the following
No crash.
libcurl version
#define LIBCURL_VERSION "7.64.0"
#define LIBCURL_TIMESTAMP "2019-02-06"
Operating system
Windows >= Vista
What I think happens
This is probably a data race in Curl_now. When two or more threads call Curl_now at the same time, and it's never been called before, the "first" thread will enter the if(isVistaOrGreater == -1) block. That first thread will set isVistaOrGreater = 1 before setting the static freq variable. (Strictly the order is undefined, because no synchronization is used. But because QueryPerformanceCounter is an external function, the compiler will normally generate code that sets isVistaOrGreater = 1 before calling QueryPerformanceCounter to initialize freq. Even though freq is a function local static).
So the window of vulnerability is from when the first thread sets isVistaOrGreater = 1 to when QueryPerformanceCounter actually writes the result into the freq variable. Not particularly long, but this can easily be a few dozen or hundred cycles (and of course much much longer if there's a context switch).
If a second thread executes Curl_now during that time window, it will see isVistaOrGreater = 1, skip the initialization and go on to divide by freq -- which is still zero at that time. The integer division by zero then causes a CPU exception/trap/whatever which is handled by the OS and converted into a structured exception (SEH).
Side note: When dividing by zero we would normally expect STATUS_INTEGER_DIVIDE_BY_ZERO. The reason we see STATUS_INTEGER_OVERFLOW is probably that very shortly after the division by zero, the memory location for freq (which is the divisor) is set to a non-zero value. So when the "integer division exception" handler of the Windows kernel later reads the divisor, it will find it non-zero, and therefor think there was an integer overflow. See https://blogs.msdn.microsoft.com/oldnewthing/20141002-00/?p=43933/ for some more information about the topic.
Calling Curl_now during curl_global_init would probably fix the issue.
Activity
bagder commentedon Feb 14, 2019
Right, the use of statics like that is not a good idea. We need something better... /cc @pps83
bagder commentedon Feb 14, 2019
I propose we just make it a build-time decision: if someone wants to build libcurl for Windows pre-vista, they need to set some define or something and then we go with the old code and for the rest we go with the modern take.
pgroke-dt commentedon Feb 14, 2019
You could also use thread safe function local static init. Something like:
MarcelRaad commentedon Feb 14, 2019
That define already exists:
_WIN32_WINNTbeing < 0x600 :-)bagder commentedon Feb 14, 2019
Another take could be to store the dynamically detected windows version in
win32_init(), called fromcurl_global_init()since that is only done once and already documented to not be thread-safe.pgroke-dt commentedon Feb 14, 2019
Not only the Windows version but also the QPC frequency ;)
bagder commentedon Feb 14, 2019
Written blindly, but perhaps just something like this:
Sorry, that's incomplete
pgroke-dt commentedon Feb 14, 2019
I personally wouldn't want to introduce a compile time switch for just that.
Any in your
#ifversion, where do you initfreq? If you can initfreqwithout a race you can also init the "is vista" information the same way.pgroke-dt commentedon Feb 14, 2019
How about
and calling
Curl_init_qpcfromwin32_init?Curl_now: figure out windows version in win32_init
bagder commentedon Feb 14, 2019
#3573 moves the initialization of the variables to the init function. I have not tested this, so I will certainly appreciate feedback and corrections if any!
pgroke-dt commentedon Feb 14, 2019
Looks good to me.
pps83 commentedon Feb 16, 2019
Sorry for coming late to the party ;)
I totally agree with that. In my project we totally have to support WinXP (heavy install base in China), but most of our users use modern Windows versions. Also, that's a minor impl detail to have compilation time change for that.
The change/fix is looks good, sorry for introducing regression.
bagder commentedon Feb 16, 2019
No worries @pps83, several of us missed that. Thanks for checking out the fix!