libexpr-c: fix EvalState pointer passed to primop callbacks#15300
Conversation
f69be29 to
8d53913
Compare
|
Hm, shouldn't we instead reorder some fields maybe so that it's the first memeber? Then this won't be UB since you are allowed to do reinterpret_cast between the pointer to first member and the object itself. |
|
Note that it wouldn't break the C ABI, since it's just an opaque pointer from that PoV. |
8d53913 to
3879e05
Compare
| */ | ||
| inline EvalState * nix_capi_eval_state_from_inner(nix::EvalState & state) | ||
| { | ||
| return reinterpret_cast<EvalState *>(reinterpret_cast<char *>(&state) - offsetof(EvalState, state)); |
There was a problem hiding this comment.
Somewhat skeptical of doing it this way. Let's reorder the members so that the nix::EvalState is the first one of the struct and keep the reinterpret_cast to the first member.
There was a problem hiding this comment.
Tried that and this breaks unsafe_new_with_self, see message below.
This gives me a segfault: looks like unsafe_new_with_self needs a specific order and EvalState takes the |
Could we put those behind std::unique_ptr and initialise before calling into unsafe_new_with_self? That way the pointers are stable and we can massage the initialisation order to be right. |
|
|
| nix_string_context ctx{context}; | ||
| nix_string_return res{""}; | ||
| desc.printValueAsJSON(v, (EvalState *) &state, strict, &ctx, copyToStore, &res); | ||
| desc.printValueAsJSON(v, nix_capi_eval_state_from_inner(state), strict, &ctx, copyToStore, &res); |
There was a problem hiding this comment.
What about the case where the eval isn't called from the C API but by nix itself like with plugins? Seems like we need to support for use-cases. Maybe let's try #15300 (comment)?
e57e9bb to
a6922cc
Compare
|
It's still unclear to me how a C plugin registering a builtin via |
a6922cc to
2dc31e2
Compare
Don't know how to solve this either... |
I guess we need to agree on the same common initial sequence of members and/or common structure that is available always. Will take a stab at it. |
bf2051a to
e2c117a
Compare
I am also not happy with this code :( |
Primop callbacks and external value methods received an inner nix::EvalState* instead of the C API EvalState* wrapper, causing segfaults when passed to functions like nix_alloc_value(). The old code cast (EvalState *) &state directly from the inner nix::EvalState& reference, but the C API wrapper struct has fetchSettings/settings/statePtr fields before the state member, so C API functions accessed memory at wrong offsets. Introduce EvalStateRef, a lightweight non-owning wrapper that constructs a proper EvalState from a nix::EvalState& on the stack. This avoids UB (no pointer arithmetic or reinterpret_cast tricks), works correctly for C plugins where nix::EvalState is created by C++ code rather than nix_eval_state_build(), and has the same lifetime guarantees as the previous approach since the wrapper lives on the stack for the duration of the callback.
e2c117a to
14d8f74
Compare
|
I think #15383 does a bit better on the surprise factor - no implicit struct layout equivalence requirements. |
|
This is a better solution to this: #15383 |
nix_c_primop_wrapper cast nix::EvalState& directly to the C API wrapper EvalState*, but the wrapper has fetchSettings and settings fields before the inner nix::EvalState member. C API functions like nix_alloc_value() then accessed state->state at the wrong offset, causing a segfault.
Use offsetof to recover the enclosing wrapper from the inner member. Same fix applied to NixCExternalValue::printValueAsJSON/printValueAsXML.