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

Fix ~basic_json causing std::terminate #4654

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

Ignition
Copy link

@Ignition Ignition commented Feb 17, 2025

During ~basic_json, it was possible to receive an exception from the allocation subsystem (quota/policy violation etc). This would cause std::terminate because destructors are noexcept.
This fix will gracefully handle that case by allowing RAII to handle anything within the current stack (used to avoid recursion), then fallback to the recursion approach for anything that still remains.

fixes #4653

@Ignition Ignition requested a review from nlohmann as a code owner February 17, 2025 21:59
@Ignition Ignition force-pushed the fix_4653 branch 3 times, most recently from 683c809 to ee5b79c Compare February 17, 2025 22:39
@coveralls
Copy link

coveralls commented Feb 18, 2025

Coverage Status

coverage: 99.17% (-0.02%) from 99.186%
when pulling 8103283 on Ignition:fix_4653
into 0b6881a on nlohmann:develop.

@Ignition Ignition force-pushed the fix_4653 branch 3 times, most recently from b4da845 to 5afd7be Compare February 18, 2025 14:15
Copy link

🔴 Amalgamation check failed! 🔴

The source code has not been amalgamated. @Ignition
Please read and follow the Contribution Guidelines.

@Ignition Ignition force-pushed the fix_4653 branch 2 times, most recently from 8bf4770 to cd04272 Compare February 18, 2025 16:05
@Ignition
Copy link
Author

@nlohmann I'm having trouble making the Windows build correct. I don't have windows machine available to debug/diagnose.

std::move(array->begin(), array->end(), std::back_inserter(stack));
}
else
#ifdef __cpp_exceptions
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, this is how we detect exceptions at other parts in the code:

#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change that and push up the change

@nlohmann
Copy link
Owner

@nlohmann I'm having trouble making the Windows build correct. I don't have windows machine available to debug/diagnose.

I don't use Windows myself. :-/

If there is an allocation problem in `destroy` fallback to recursion
approach.

Signed-off-by: Gareth Lloyd <[email protected]>
@gregmarr
Copy link
Contributor

I'm not wild about this idea. It's swallowing unknown exceptions and setting up a stack overflow.

@nlohmann
Copy link
Owner

I am not sure what to make of this right now. The noexcept annotation was from times where we did hardly anything in the destructor.

@gregmarr
Copy link
Contributor

Destructors are noexcept by default.

@Ignition
Copy link
Author

I'm not wild about this idea. It's swallowing unknown exceptions and setting up a stack overflow.

Normally I would fully agree about swallowing unknown exceptions being bad. But in this case with the current implementation there are only a few potential sources of exceptions.

  • allocation subsystem, should only be able to throw an exception via allocate or construct, I have yet to see a use case where destruct or deallocate throw (for the same reasons that destructors are noexcept)
  • iteration, which doesn't ATM throw, and a good implementation shouldn't ever need to throw
  • moves, which are noexcept so no exceptions there either

So it just a question about is it safe to swallow unknown exception from the allocation subsystem. My feeling is yes, because its actually not swallowing, but handling a corner case even if you don't know what that exception is.

You either:

  • Do nothing with the exception, hit noexcept, std::terminate which kills the program which is bad.
  • Make ~basic_json noexcept(false), which I DO NOT recommend. It just moves the problem onto the consumer, any type that has json as a data member would also need a noexcept(false) destructor.
  • Handle the unknown exception, in this case we are confident the exception could have only come from the memory subsystem. Regardless of the actual exception, we can handle it by letting RAII do it job and then fallback to doing regular recursive cleanup. This does risk stack exhaustion, but that is much better than std::terminate.

@gregmarr
Copy link
Contributor

Do you use the JSON_DIAGNOSTICS define? If not, would you be okay with using that option?

I think it's possible to rewrite this to not use the vector if the m_parent field exists, using an algorithm based on Morris Traversal. The basic idea is that you have a current pointer. You start at the root (this), and walk the current pointer down the tree. When you find an object/array, you look at the first object element or the last array element. If it's a leaf node, remove it from the object/array. By using the last array element instead of the first, you are just doing a pop_back on the array instead of moving everything after it. If it's an object/array, you update current and repeat. Once the object/array is empty, you update current to be its parent, and then remove the thing you just processed from its object/array.

I haven't written any of this code, just thinking about what might be possible.

@Ignition
Copy link
Author

Ignition commented Feb 24, 2025

@gregmarr @nlohmann
I have delivered a patch in my build that prevents std::terminate in my product. Unfortunate I don't have the time to spend on getting this merged here. I maybe will come back to this PR, but don't bet/rely on it.

@gregmarr I agree it is possible to do it without heap allocation. I think that modification is worth doing (assuming benchmarks show that is fine). Here is Herb Sutter talking about destroying trees https://youtu.be/JfmTagWcqoE?si=XyJR0YpYZGm4FGbB&t=970

@gregmarr
Copy link
Contributor

@Ignition Yeah, the current method is the bottom row of the table, O(N) heap space with moves. In order to remove the O(N) heap space, which is the issue that you're having of allocating memory in a destructor, we'd have to go to the second row of the table, which is O(N log N) time.

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

Successfully merging this pull request may close these issues.

~basic_json can terminate program
4 participants