Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Segfault on nested parsing #778

Closed
quicknir opened this issue Oct 11, 2017 · 26 comments
Closed

Segfault on nested parsing #778

quicknir opened this issue Oct 11, 2017 · 26 comments

Comments

@quicknir
Copy link

It's a bit difficult to provide a full repro here as it would mean tearing out a bunch of code, but the gist of the bug is as follows:

struct Foo { int x; bool y};
// define from_json; use f.x = j.at("x").get<int>(), etc
    auto j = json::parse(R"(
              { "m": {"one": {"x": 1},
                      "two": {"x": 2, "y": false }},
                "s": "arg"}
             )");
std::unordered_map<std::string, Foo> m = j.at("m");

The last line should throw IMHO, but in fact it segfaults. I think I'm on version 2.1.1.

@nlohmann
Copy link
Owner

I cannot reproduce this with the following code running Xcode 9.0 with Address Sanitizer and Undefined Behavior Sanitizer:

#include "json.hpp"
#include <iostream>
#include <unordered_map>

using json = nlohmann::json;

struct Foo { int x; bool y; };

void from_json(const json& j, Foo& f) {
    f.x = j.at("x").get<int>();
    try {
        f.y = j.at("y");
    } catch(...) {
        f.y = false;
    }
}

int main()
{
    auto j = json::parse(R"(
                         { "m": {"one": {"x": 1},
        "two": {"x": 2, "y": false }},
                             "s": "arg"}
                         )");
    
    std::unordered_map<std::string, Foo> m = j.at("m");
    
    for (auto el : m)
    {
        std::cout << el.first << " -> (" << el.second.x << ","
          << std::boolalpha << el.second.y << ")" << std::endl;
    }
}

Maybe there is something wrong with your from_json function?

@quicknir
Copy link
Author

Sorry, why the try/catch around y? If you remove that, does it make a difference? I am generating the from_json based on reflection, but I'll try to write it out explicitly and see if I can reproduce.

@nlohmann
Copy link
Owner

In your example, the object at key one does not have an entry with key y. Without the try/catch block, accessing j.at("y") would raise an exception.

@quicknir
Copy link
Author

quicknir commented Oct 11, 2017

Okay, here's what I have now:

struct Blub {
    int x;
    bool y;
};

void from_json(const ::nlohmann::json & j, Blub& t) {
    t.x = j.at("x").get<int>();
    t.y = j.at("y").get<bool>();
}

TEST(jsonable, nested_dict_fail)
{
    auto j = json::parse(R"(
              { "m": {"one": {"x": 1},
                      "two": {"x": 2, "y": false }},
                "s": "arg"}
             )");

    std::unordered_map<std::string, Blub> m = j.at("m");
}

Once again, this segfaults for me. This is run in the context of a gtest suite but it's hard to believe that could be making the difference. And since your library is header only, we couldnt' have built it incorrectly locally.

@quicknir
Copy link
Author

quicknir commented Oct 11, 2017

Below is the backtrace (collapsed cause its huge).

Backtrace
#0  0x00007fffe4549495 in raise () from /lib64/libc.so.6
#1  0x00007fffe454ac75 in abort () from /lib64/libc.so.6
#2  0x00007fffe45873a7 in __libc_message () from /lib64/libc.so.6
#3  0x00007fffe458cdee in malloc_printerr () from /lib64/libc.so.6
#4  0x00007fffe458fc3d in _int_free () from /lib64/libc.so.6
#5  0x000000000042a361 in __gnu_cxx::new_allocator<std::__detail::_Hash_node_base*>::deallocate (this=<synthetic pointer>, 
    __p=<optimized out>) at /spare/local/nir/venv-mosaic/gcc/5.4.0/include/c++/5.4.0/ext/new_allocator.h:110
#6  std::allocator_traits<std::allocator<std::__detail::_Hash_node_base*> >::deallocate (__a=<synthetic pointer>, __n=<optimized out>, 
    __p=<optimized out>) at /spare/local/nir/venv-mosaic/gcc/5.4.0/include/c++/5.4.0/bits/alloc_traits.h:517
#7  std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub>, true> > >::_M_deallocate_buckets (this=0x7fffffffcca0, 
    __n=<optimized out>, __bkts=<optimized out>) at /spare/local/nir/venv-mosaic/gcc/5.4.0/include/c++/5.4.0/bits/hashtable_policy.h:2010
#8  std::_Hashtable<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub>, std::allocator<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub> >, std::__detail::_Select1st, std::equal_to<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_deallocate_buckets (__n=<optimized out>, __bkts=<optimized out>, 
    this=0x7fffffffcca0) at /spare/local/nir/venv-mosaic/gcc/5.4.0/include/c++/5.4.0/bits/hashtable.h:356
#9  std::_Hashtable<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub>, std::allocator<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub> >, std::__detail::_Select1st, std::equal_to<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_deallocate_buckets (this=0x7fffffffcca0)
    at /spare/local/nir/venv-mosaic/gcc/5.4.0/include/c++/5.4.0/bits/hashtable.h:361
#10 std::_Hashtable<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub>, std::allocator<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub> >, std::__detail::_Select1st, std::equal_to<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::~_Hashtable (this=0x7fffffffcca0, __in_chrg=<optimized out>)
    at /spare/local/nir/venv-mosaic/gcc/5.4.0/include/c++/5.4.0/bits/hashtable.h:1226
#11 0x0000000000430f23 in std::_Hashtable<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub>, std::allocator<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub> >, std::__detail::_Select1st, std::equal_to<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_Hashtable<std::_Rb_tree_const_iterator<std::pair<const std::basic_string<char>, nlohmann::basic_json<> > > > (__bucket_hint=0, __h1=..., __h2=..., __h=..., __eq=..., __exk=..., __a=..., __l=..., 
    __f=..., this=0x7fffffffcca0) at /spare/local/nir/venv-mosaic/gcc/5.4.0/include/c++/5.4.0/bits/hashtable.h:823
#12 std::_Hashtable<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub>, std::allocator<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub> >, std::__detail::_Select1st, std::equal_to<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_Hashtable<std::_Rb_tree_const_iterator<std::pair<const std::basic_string<char>, nlohmann::basic_json<> > > > (__hf=..., __eql=..., __a=..., __n=0, __l=..., __f=..., this=0x7fffffffcca0)
    at /spare/local/nir/venv-mosaic/gcc/5.4.0/include/c++/5.4.0/bits/hashtable.h:437
#13 std::unordered_map<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, mosaic::util::testing::(anonymous namespace)::Blub, std::hash<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub> > >::unordered_map<std::_Rb_tree_const_iterator<std::pair<const std::basic_string<char>, nlohmann::basic_json<> > > > (__hf=..., __eql=..., __a=..., __n=0, __last=..., __first=..., this=0x7fffffffcca0)
    at /spare/local/nir/venv-mosaic/gcc/5.4.0/include/c++/5.4.0/bits/unordered_map.h:168
#14 nlohmann::detail::from_json<nlohmann::basic_json<>, std::unordered_map<std::basic_string<char>, mosaic::util::testing::(anonymous namespace)::Blub>, 0> (obj=std::unordered_map with 0 elements, j=...)
    at /spare/local/nir/venv-mosaic/json/2.1.1.1/include/nlohmann/json.hpp:784
#15 nlohmann::detail::from_json_fn::call<nlohmann::basic_json<>, std::unordered_map<std::basic_string<char>, mosaic::util::testing::(anonymous namespace)::Blub> > (this=<optimized out>, val=std::unordered_map with 0 elements, j=...)
    at /spare/local/nir/venv-mosaic/json/2.1.1.1/include/nlohmann/json.hpp:864
#16 nlohmann::detail::from_json_fn::operator()<nlohmann::basic_json<>, std::unordered_map<std::basic_string<char>, mosaic::util::testing::(anonymous namespace)::Blub> > (this=<optimized out>, val=std::unordered_map with 0 elements, j=...)
    at /spare/local/nir/venv-mosaic/json/2.1.1.1/include/nlohmann/json.hpp:879
#17 nlohmann::adl_serializer<std::unordered_map<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, mosaic::util::testing::(anonymous namespace)::Blub, std::hash<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::bas---Type <return> to continue, or q <return> to quit---
ic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, mosaic::util::testing::(anonymous namespace)::Blub> > >, void>::from_json<const nlohmann::basic_json<>&, std::unordered_map<std::basic_string<char>, mosaic::util::testing::(anonymous namespace)::Blub> > (val=std::unordered_map with 0 elements, 
    j=...) at /spare/local/nir/venv-mosaic/json/2.1.1.1/include/nlohmann/json.hpp:926
#18 nlohmann::basic_json<std::map, std::vector, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, bool, long, unsigned long, double, std::allocator, nlohmann::adl_serializer>::get<std::unordered_map<std::basic_string<char>, mosaic::util::testing::(anonymous namespace)::Blub>, std::unordered_map<std::basic_string<char>, mosaic::util::testing::(anonymous namespace)::Blub>, 0> (
    this=<optimized out>) at /spare/local/nir/venv-mosaic/json/2.1.1.1/include/nlohmann/json.hpp:3237
#19 nlohmann::basic_json<std::map, std::vector, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, bool, long, unsigned long, double, std::allocator, nlohmann::adl_serializer>::operator std::unordered_map<std::basic_string<char>, mosaic::util::testing::(anonymous namespace)::Blub><std::unordered_map<std::basic_string<char>, mosaic::util::testing::(anonymous namespace)::Blub>, 0> (
    this=<optimized out>) at /spare/local/nir/venv-mosaic/json/2.1.1.1/include/nlohmann/json.hpp:3495
#20 mosaic::util::testing::(anonymous namespace)::jsonable_nested_dict_fail_Test::TestBody (this=<optimized out>)
    at polygon/cpp/mosaic/util/jsonable.t.cpp:175
#21 0x00007fffeaa23453 in void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) () from /spare/local/nir/venv-mosaic/googletest/1.7.0.1-c++14-gcc5/lib/libgtest.so
#22 0x00007fffeaa13c67 in testing::Test::Run() () from /spare/local/nir/venv-mosaic/googletest/1.7.0.1-c++14-gcc5/lib/libgtest.so
#23 0x00007fffeaa13d08 in testing::TestInfo::Run() () from /spare/local/nir/venv-mosaic/googletest/1.7.0.1-c++14-gcc5/lib/libgtest.so
#24 0x00007fffeaa13e15 in testing::TestCase::Run() () from /spare/local/nir/venv-mosaic/googletest/1.7.0.1-c++14-gcc5/lib/libgtest.so
#25 0x00007fffeaa158ef in testing::internal::UnitTestImpl::RunAllTests() ()
   from /spare/local/nir/venv-mosaic/googletest/1.7.0.1-c++14-gcc5/lib/libgtest.so
#26 0x00007fffeaa15c1c in testing::UnitTest::Run() () from /spare/local/nir/venv-mosaic/googletest/1.7.0.1-c++14-gcc5/lib/libgtest.so
#27 0x00007fffed23cb22 in main () from /spare/local/nir/venv-mosaic/googletest/1.7.0.1-c++14-gcc5/lib/libgtest_main.so
#28 0x00007fffe4535d1d in __libc_start_main () from /lib64/libc.so.6
#29 0x000000000042cad9 in _start ()

@gregmarr
Copy link
Contributor

As @nlohmann said above, the object at key "one" does not have an entry with key "y". Without the try/catch block, accessing j.at("y") raises an exception. You either need a try/catch block in your from_json function or you need to check to see if the key exists before you try to access the value.

@quicknir
Copy link
Author

@gregmarr An exception would not cause a segfault running inside of a gtest. I've also just experience this in an executable I wrote where everything was wrapped in a try catch (const std::exception& e)

@gregmarr
Copy link
Contributor

Are you saying that gtest should be catching that std::out_of_range exception that at("y") is throwing and just reporting a test failure?
Did you try the code above that @nlohmann said worked for him?

@quicknir
Copy link
Author

@gregmarr gtest tests catch everything, yes; it would be bad if a single exception in a single test segfaulted and stopped the rest of the tests from running, right? And I also tested it in an application. I can test it in a simple main as well, just a little more involved for me, but I'll try that now.

@quicknir
Copy link
Author

quicknir commented Oct 11, 2017

@gregmarr this program segfaults, even though it clearly shouldn't:

#include <nlohmann/json.hpp>
#include <iostream>
#include <unordered_map>

using json = nlohmann::json;

struct Foo { int x; bool y; };

void from_json(const json& j, Foo& f) {
    f.x = j.at("x").get<int>();
    f.y = j.at("y");
}

int main()
{
    try {
        auto j = json::parse(R"(
                         { "m": {"one": {"x": 1},
        "two": {"x": 2, "y": false }},
                             "s": "arg"}
                         )");

        std::unordered_map<std::string, Foo> m = j.at("m");
    }
    catch (...) {}
}

Interestingly I tried with gcc (6.3, 5.4) and clang (3.7), it doesn't segfault with clang.

@gregmarr
Copy link
Contributor

Can you try replacing std::unordered_map<std::string, Foo> m = j.at("m"); with auto m = j.at("m");? There's something weird in the stack trace.

@quicknir
Copy link
Author

quicknir commented Oct 11, 2017

If I change it to auto then it does work. I'm not sure what the type of m is now though, I assume it's just a json object and therefore none of the from_json code has even been run.

@jaredgrubb
Copy link
Contributor

It could be that there's two exceptions getting thrown. That would force a std::terminate even with a outer try-catch. I haven't been able to find where that could happen in the example, but just throwing the idea out there in case someone else can?

@quicknir
Copy link
Author

Can someone maybe try to use the exact code as me, and gcc, and try to reproduce?

@gregmarr
Copy link
Contributor

There's an abort in the stdlib:
#0 raise () from /lib64/libc.so.6
#1 abort () from /lib64/libc.so.6
#2 __libc_message () from /lib64/libc.so.6
#3 malloc_printerr () from /lib64/libc.so.6
#4 _int_free () from /lib64/libc.so.6
#5 __gnu_cxx::new_allocatorstd::__detail::_Hash_node_base*::deallocate (this=, __p=)

The type of m should be json.

@gregmarr
Copy link
Contributor

gregmarr commented Oct 11, 2017

So there's this function in the call stack:

std::unordered_map<std::string, Blub>::unordered_map<std::const_iterator<std::pair<const std::basic_string<char>, nlohmann::basic_json<>>>>(first, last, n=0, a, eq, hf)

So it's trying to construct the std::unordered_map<std::string, Blub> using this constructor:

template<class InputIt>unordered_map(InputIt first, InputIt last, size_type bucket_count, const Hash& hash, const KeyEqual& equal, const Allocator& alloc);

with InputIt equal to std::const_iterator<std::pair<const std::basic_string<char>, nlohmann::basic_json<>>>, and failing spectacularly while destroying an internal class.

@gregmarr
Copy link
Contributor

I'll bet what's needed here is a proper from_json function for std::unordered_map<std::string, Blub>.

@gregmarr
Copy link
Contributor

gregmarr commented Oct 11, 2017

#14 nlohmann::detail::from_json<nlohmann::basic_json<>, std::unordered_map<std::string, Blub>, 0> (obj=std::unordered_map with 0 elements, j=...) at .../json/2.1.1.1/include/nlohmann/json.hpp:784
         enable_if_t<is_compatible_object_type<BasicJsonType, CompatibleObjectType>::value, int> = 0>
void from_json(const BasicJsonType& j, CompatibleObjectType& obj)
{
    if (not j.is_object())
    {
        JSON_THROW(std::domain_error("type must be object, but is " + j.type_name()));
    }

    auto inner_object = j.template get_ptr<const typename BasicJsonType::object_t*>();
    using std::begin;
    using std::end;
    // we could avoid the assignment, but this might require a for loop, which
    // might be less efficient than the container constructor for some
    // containers (would it?)
    obj = CompatibleObjectType(begin(*inner_object), end(*inner_object));
}

This is the from_json function that is being used.

@quicknir
Copy link
Author

quicknir commented Oct 11, 2017 via email

@gregmarr
Copy link
Contributor

Can you try with this function:

void from_json(const json& j, Foo& f) {
    auto x = j.find("x");
    if (x != j.end()) {
        f.x = (*x).get<int>();
    } else {
        f.x = 0.0;
    }
    auto y = j.find("y");
    if (y != j.end()) {
        f.y = (*y).get<bool>();
    } else {
        f.y = false;
    }
}

@quicknir
Copy link
Author

I didn't, but I think it's pretty clear that won't cause the problem. However, I did manage to write up the following json agnostic example: https://wandbox.org/permlink/G5u9xZWCvsQLeRvz. This segfaults in gcc6.3 but not in 7 series. My readings on the matter seem to indicate that this is a standard library bug; its not safe to assume that iterators can't throw. I'm going to do one more test to make sure that json does the expected thing with gcc7 and then I think we can probably close it as an upstream issue.

@gregmarr
Copy link
Contributor

This tells me that this is exactly the problem. We KNOW that the from_json function that you wrote throws when processing "one", and that the from_json function you wrote is going to be called during the automatically generated from_json used to construct the std::unordered_map from the std::map. While I agree that it's probably an upstream issue, I think you can avoid the upstream issue completely by making your from_json function safe as I wrote.

@quicknir
Copy link
Author

A correction on my previous comment, it's actually gcc 8 seemingly that fixes this bug. Is it possible to easily access json on wandbox somehow? Or if someone here has local access to gcc 8, maybe they could test?

@gregmarr I agree it's a valid workaround, that is clear. But that's what it is: a workaround. Writing from_json for unordered_map<string, Foo> is boilerplate, pure and simple, that shouldn't be necessary.

At any rate it seems unlikely that there's still an issue present since with clang code works as expected, and gcc8 is a pain to try and test with, so I'll close this for now.

@gregmarr
Copy link
Contributor

This has nothing to do with from_json for unordered_map<string, Foo>. That was a guess I made at one point, but it probably isn't actually necessary. It's for Foo itself. It's about making a from_json for Foo that won't throw on known input, exactly as @nlohmann wrote on the very first reply to this issue, and explained in his second reply when you asked him why he wrote it that way. I provided a second version that does it without try/catch, that will be easier to generate, since you mentioned that you were generating these. If you don't fix this problem, then even with the other compilers not crashing, it still doesn't properly read the JSON, and you'll miss data.

@quicknir
Copy link
Author

quicknir commented Oct 12, 2017

@gregmarr No data will be missed, it throws an exception. That's the whole point, that's the error handling mechanism. That exception will bubble up. If you simply try to de-serialize a Foo and you are missing a field, it will throw. I don't understand what the problem with this is? Foo works perfectly. vector<Foo> will work perfectly too. Only unordered_map<string, Foo> will not work correctly, but only because the of gcc bug. If not for the bug, then unordered_map<string, Foo> m = j.at("m"); will throw an exception on incomplete data, without writing anything. This is exactly what you would expect and hope, if Foo's solution to incomplete data is to throw!

What's more, there isn't really any other way to handle errors. from_json doesn't return an error code or offer the user anything else. Defaulting fields to values is not a substitute for error handling. So if exceptions will be the only error handling mechanism, it needs to work!

@gregmarr
Copy link
Contributor

Ah, so you're saying that all the fields are in fact required, and you're not expecting to handle the case you've given. I thought you actually wanted to read the sample file. Thanks for the clarification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants