A few years ago I did a lot of thinking and writing about floating-point math. It was good fun, and I learned a lot in the process, but sometimes I go a long time without actually using that hard-earned knowledge. So, I am always inordinately pleased when I end up working on a bug which requires some of that specialized knowledge. Here then is the first of (at least) three tales of floating-point bugs that I have investigated in Chromium. This is a short one.

Apparently the official JSON logo?The title of the bug was “JSON Parses 64-bit Integers Incorrectly”, which doesn’t immediately sound like a floating-point or browser issue, but it was filed in crbug.com and I was asked to take a look. The simplest version of the repro is to open the Chrome developer tools (F12 or Ctrl+Shift+I) and paste this code into the developer console:

json = JSON.parse(‘{“x”: 2940078943461317278}’); alert(json[‘x’]);

Pasting unknown code into the console window is a good way to get pwned but this code was simple enough that I could tell that it wasn’t malicious. The bug report was nice enough to have included the author’s expectations and actual results:

What is the expected behavior?

The integer 2940078943461317278 should be returned.

What went wrong?

The integer 2940078943461317000 is returned instead.

The “bug” was actually reported on Linux and I work on Chrome for Windows but the behavior is cross-platform and I have some floating-point expertise so I investigated.

The reason that this behavior of integers is a potential “floating-point” bug is because JavaScript doesn’t actually have an integer type. That is also the reason that this isn’t actually a bug.

Speaking of big things, this is a pyramid!The input number is quite big. It is about 2.9e18. And that is the problem. Since JavaScript doesn’t have an integer type it uses IEEE-754 floating-point double-precision for its numbers. This binary floating-point format has a sign bit, an 11-bit exponent, and a 53-bit mantissa (yes, that’s 65 bits, the hidden implied one is magic). This double type works well enough for storing integers that many JavaScript programmers never notice that there isn’t an integer type, but very large numbers break the illusion.

A JavaScript number can exactly store any integer up to 2^53. After that it can hold all even integers up to 2^54. And after that it can hold all multiples of four up to 2^55, and so on.

The problematic number expressed in base-2 scientific notation, it is about 1.275 * 2^61. At that range very few integers can be expressed – the gap between representable numbers is 512. Here are three relevant numbers:

  • 2,940,078,943,461,317,278 – the number the bug filer wanted to store
  • 2,940,078,943,461,317,120 – the closest double to that number (smaller)
  • 2,940,078,943,461,317,632 – the next closest double to that number (larger)

The number in question is bracketed by two doubles and the JSON module (like JavaScript itself, or any other correctly implemented text-to-double conversion function) did the best that it could and returned the closest double. To be clear, the number that the bug filer wanted to store cannot be stored in the built-in JavaScript numeric type.

So far so good. If you push the limits of the language you need to know more about how it works. But there is one remaining mystery. The bug report said that the number returned was actually this one:

2,940,078,943,461,317,000

That is peculiar because it is not the input number, it is not the closest double and, in fact, it is not even a number that is representable as a double!

Mystery Bay, NSW, AustraliaThis mystery is also explained by the JavaScript specification. The spec says that when printing a number the implementation should print enough digits to uniquely identify it, and then no more. This is handy when printing numbers like 0.1 which cannot be exactly represented as a double. For instance, if JavaScript mandated that 0.1 should be printed as the value stored then it would have to print:

0.1000000000000000055511151231257827021181583404541015625

This would be accurate but it would just confuse people without adding any value. The exact rules can be found here (search for “ToString Applied to the Number Type”). I don’t think they actually require the trailing zeroes, but they certainly allow them.

So, the JavaScript runtime prints 2,940,078,943,461,317,000 because:

  • The value of the original number was lost when it was stored as a JavaScript number
  • The printed number is close enough to the stored value to uniquely identify it
  • The printed number is the simplest number that uniquely identifies the stored value

Working-as-intended, not a bug, closed as WontFix. The original bug can be found here.

Reddit discussion is here, twitter announcement here.

About brucedawson

I’m a programmer, working for Google, focusing on optimization and reliability. Nothing’s more fun than making code run 10x as fast. Unless it’s eliminating large numbers of bugs.

I also unicycle. And play (ice) hockey. And sled hockey. And juggle. And worry about whether this blog should have been called randomutf-8.

2010s in review tells more: https://twitter.com/BruceDawson0xB/status/1212101533015298048

Read More

ترك الرد

من فضلك ادخل تعليقك
من فضلك ادخل اسمك هنا