Home Dashboard Directory Help
Search

System.Diagnostics.StopWatch elapsed time affected by user changing computer's date/time by Strilanc


Status: 

Closed
 as Won't Fix Help for as Won't Fix


2
0
Sign in
to vote
Type: Bug
ID: 741848
Opened: 5/12/2012 8:11:05 PM
Access Restriction: Public
Moderator Decision: Sent to Engineering Team for consideration
1
Workaround(s)
view
1
User(s) can reproduce this bug

Description

According to reflector, the System.Diagnostics.StopWatch class uses DateTime.UtcNow when a high resolution timer is not available. This means that a user can control the elapsed time by editing their system time.

This issue makes the stop watch class an insecure method of measuring elapsed time. A user can even make the elapsed time negative, which could trigger serious bugs because developers are unlikely to consider that case!

One potential solution is to fallback to Environment.TickCount instead of DateTime.UtcNow.

This bug may be present in other places in the framework. Anywhere multiple queries to the system time are operated upon.
Details
Sign in to post a comment.
Posted by Nick Lowe on 5/24/2014 at 1:42 AM
So, a further needed clarification. Windows XP SP3 actually does not supply a usable value large than 32-bits.

A disassembly of GetTickCount yields:

    0:000> u kernel32!GetTickCount
    kernel32!GetTickCount:
    7c80934a ba0000fe7f     mov     edx,offset SharedUserData (7ffe0000)
    7c80934f 8b02            mov     eax,dword ptr [edx]
    7c809351 f76204         mul     eax,dword ptr [edx+4]
    7c809354 0facd018        shrd    eax,edx,18h
    7c809358 c3             ret

This therefore produces a 64-bit intermediate result, but the "shrd" only fixes up eax, so only 32 bits of the result are useful.
You still have the 49-day limit.

In Windows Server 2003, however, the shrd is followed by a "shr edx, 18h", which means Server 2003 does return a 64-bit result. Because of the shift in the conversion of "scheduler ticks" to "milliseconds" that you only get 40 useful bits of precision, which means it will roll over after 34 years.
Posted by Nick Lowe on 5/21/2014 at 9:36 AM
The plot further thickens, even back in Windows XP, certainly SP2 and later potentially earlier, GetTickCount returns a 64-bit value via the normal calling conventions.

We can validate this with a debugger:

The return value is written in to EAX (low part) and EDX (high part).
Under 64-bit Windows, it is additionally written to RAX.

While this is undocumented behaviour, we are free therefore to use:

[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern long GetTickCount();

Why is this not documented? And why did/does .NET not take advantage of this?
Posted by Nick Lowe on 5/20/2014 at 2:48 AM
I have re-reported this issue as the rationale for not fixing this is no longer valid now that Windows XP and Server 2003 is no longer supported as of .NET 4.5

https://connect.microsoft.com/VisualStudio/feedback/details/877956/

The reason that this appears to not have been fixed previously is due to the overflows that occur in the value returned from the Environment.TickCount property, Win32's GetTickCount.
Those overflows make the 32-bit tick count value unusable without a dedicated slept thread within the Stopwatch class that wakes up appropriately to handle the eventuality. This clearly makes that approach not viable due to the overheads it imposes.

This is no longer an issue as Windows XP and Windows Server 2003 are no longer supported by .NET 4.5 and newer so an Environment.TickCount64 can be used, Win32's GetTickCount64.

Once an Environment.TickCount64 is introduced meaning that overflows can no longer occur, a dedicated thread is no longer required so this bug in the Stopwatch class can be resolved without tradeoff.

I have reported the lack of an Environment.TickCount64 issue as a bug here:

https://connect.microsoft.com/VisualStudio/feedback/details/875703/
Posted by Microsoft on 5/15/2012 at 3:50 PM
Hi Strilanc!

Thanks for bringing up this issue. We are always grateful when customers point towards potential concerns - this helps us ensuring the quality of the .NET Framework and driving the product into the right direction.

Indeed, your analysis is correct. Unfortunately, we cannot address this issue in the immediate future.
However, we have logged it and we will continue thinking about addressing issue in a future release.

I thank you for your time and your contribution.

Greg
(Software Engineer on the .NET Base Class Libraries team)
Posted by Microsoft on 5/15/2012 at 3:42 AM
Thanks for your update. We are rerouting this issue to the appropriate group within the Visual Studio Product Team for triage and resolution. These specialized experts will follow-up with your issue.
Posted by Strilanc on 5/14/2012 at 7:10 AM
Here is test code. Put it inside the main class of a new C# console project.

The code periodically prints out elapsed time as measured by StopWatch, TickCount, and Date.UtcNow. If you change the date of the test machine then the Date measure will be affected and, if the system doesn't have a high resolution timer, so will the StopWatch measure (this is the bug). To make things easier the code prints out if a high resolution timer is found.

        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        private static extern bool QueryPerformanceCounter(out long freq);

        static void Main(string[] args) {
            long x;
            var hasHighRes = QueryPerformanceCounter(out x);
            Console.WriteLine("High res timer available: " + hasHighRes);
            if (hasHighRes) Console.WriteLine("Issue only appears WITHOUT high res timer");

            var s = System.Diagnostics.Stopwatch.StartNew();
            var starttick = Environment.TickCount;
            var startdate = DateTime.UtcNow;
            while (true) {
                System.Threading.Thread.Sleep(1000);
                unchecked {
                    var dtick = (uint)(Environment.TickCount - starttick);
                    var ddate = DateTime.UtcNow - startdate;
                    Console.WriteLine(String.Format(
                        "stopwatch: {0:0.0}s, tickcount: {1:0.0}s, datedif: {2:0.0}s",
                        s.Elapsed.TotalSeconds,
                        dtick/1000.0,
                        ddate.TotalSeconds));
                }
            }
        }
Posted by MS-Moderator10 [Feedback Moderator] on 5/14/2012 at 1:23 AM
Thank you for your feedback. In order to efficiently investigate and reproduce this issue, we are requesting additional information outlined below.

Could you please give us a demo project to demonstrate this issue?

Please submit this information to us within 4 business days. We look forward to hearing from you with this information.

Microsoft Visual Studio Connect Support Team
Posted by MS-Moderator01 on 5/13/2012 at 6:32 PM
Thank you for your feedback, we are currently reviewing the issue you have submitted. If this issue is urgent, please contact support directly(http://support.microsoft.com)
Sign in to post a workaround.
Posted by Qwertie on 9/25/2013 at 2:42 PM
Use Environment.TickCount instead, which counts the time from Windows startup and always goes up, rather than the wall-clock time which can change. I published this Gist with an easy-to-use Stopwatch alternative based on

https://gist.github.com/qwertie/6706409

SimpleTimer cannot be used to measure time periods longer than 24.8 days. The 32-bit millisecond counter will overflow and Millisec will become negative.