Search code examples
c++integer-overflowjsoncpp

JsonCpp do not protect from uint64 overflow and have weird behavior


I am playing around with JsonCpp and have noticed weird behavior when getting to the end of uint64 range.

Numbers in range works properly. Numbers above range but between [2**64,2**64+2**11] return 0. Numbers above 2**64+2**11 throws an exception.

Is that expected? Can I configure JsonCpp to do those validations for me?

Thanks in advance!

Below is a code snippet that demonstrate the issue.

#include <iostream>
#include <jsoncpp/json/json.h>
#include <jsoncpp/json/reader.h>
#include <jsoncpp/json/value.h>


int main() {
    //std::string uint64Str = "1234"; // any number in range [0, 2**64-1] works correctly (prints uint64Str)
    //std::string uint64Str = "18446744073709551615"; // any number in range [0, 2**64-1] works correctly, including 2**64-1 (prints uint64Str)
    //std::string uint64Str = "18446744073709551616"; //max uint64 + 1 print 0
    //std::string uint64Str = "18446744073709551617"; //max uint64 + 2 print 0
    // ...
    //std::string uint64Str = "18446744073709553663"; //max uint64 + 2048 print 0
    //std::string uint64Str = "18446744073709553664"; //max uint64 + 2049 print 0
    std::string uint64Str = "18446744073709553665"; //max uint64 + 2050 (or more) throws instance of 'Json::LogicError' what():  double out of UInt64 range
    Json::Value root;
    Json::Reader reader;
    bool parsingResult = reader.parse("{\"key\":"+uint64Str+"}", root);
        std::cout<<root["key"].asUInt64()<<std::endl;
    
        return 0;
}


Solution

  • Thanks to TedLyngmo for the guidance.

    After his answer and additional research. Numbers that are bigger than max are stored in a real (double).

    And then this code runs:

    case realValue:
          JSON_ASSERT_MESSAGE(InRange(value_.real_, 0, maxUInt64),
                              "double out of UInt64 range");
          return UInt64( value_.real_ );
    

    I would expect the function InRange to fail on numbers out of range. However its implementation is:

    template <typename T, typename U>
    static inline bool InRange(double d, T min, U max) {
      // The casts can lose precision, but we are looking only for
      // an approximate range. Might fail on edge cases though. ~cdunn
      return d >= static_cast<double>(min) && d <= static_cast<double>(max);
    }
    

    It converts max to double and it rounds max up (to 2**64+2) and if d is between max<uint64> to 2**62+2**11 is is also rounded (to 2**64+2). So it thinks it is in range. When converting d to uint64_t you get max+1 which is 0.

    The function InRange do mention that is in not exact... yet, I consider that to be a bug in the library.

    The solution that I am going to use is:

    uint64_t getUint64(const Json::Value& shouldBeUInt64) {
        uint64_t res = shouldBeUInt64.asUInt64();
        if (res == 0 and shouldBeUInt64.asDouble()>0){
            throw Json::LogicError("double out of UInt64 range");
        }
        return res;
    }
    

    Update (25 Nov 2023): See suggested fix here: https://github.com/open-source-parsers/jsoncpp/pull/1519