Skip to content

libexpr-c: fix EvalState pointer passed to primop callbacks#15300

Closed
Mic92 wants to merge 1 commit into
NixOS:masterfrom
Mic92:fix-eval-state-in-c-primops
Closed

libexpr-c: fix EvalState pointer passed to primop callbacks#15300
Mic92 wants to merge 1 commit into
NixOS:masterfrom
Mic92:fix-eval-state-in-c-primops

Conversation

@Mic92

@Mic92 Mic92 commented Feb 19, 2026

Copy link
Copy Markdown
Member

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.

@Mic92 Mic92 requested a review from edolstra as a code owner February 19, 2026 14:53
@github-actions github-actions Bot added the c api Nix as a C library with a stable interface label Feb 19, 2026
@Mic92 Mic92 force-pushed the fix-eval-state-in-c-primops branch 2 times, most recently from f69be29 to 8d53913 Compare February 19, 2026 15:07
@xokdvium

Copy link
Copy Markdown
Contributor

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.

@xokdvium

Copy link
Copy Markdown
Contributor

Note that it wouldn't break the C ABI, since it's just an opaque pointer from that PoV.

Comment thread src/libexpr-tests/nix_api_expr.cc
@Mic92 Mic92 force-pushed the fix-eval-state-in-c-primops branch from 8d53913 to 3879e05 Compare February 19, 2026 15:33
Comment thread src/libexpr-c/nix_api_expr_internal.h Outdated
*/
inline EvalState * nix_capi_eval_state_from_inner(nix::EvalState & state)
{
return reinterpret_cast<EvalState *>(reinterpret_cast<char *>(&state) - offsetof(EvalState, state));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Tried that and this breaks unsafe_new_with_self, see message below.

@Mic92

Mic92 commented Feb 19, 2026

Copy link
Copy Markdown
Member Author

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.

This gives me a segfault:

   Thread 1 "nix-expr-tests" received signal SIGSEGV, Segmentation fault.
   #0  nix::EvalState::createBaseEnv(nix::EvalSettings const&) ()
       from libnixexpr.so
   #1  nix::EvalState::EvalState(nix::LookupPath const&, ...) ()
       from libnixexpr.so
   #2  unsafe_new_with_self<EvalState, ...>()
       from libnixexprc.so
   #3  nix_state_create ()
       from libnixexprc.so
   #4  TestFactoryImpl<...nix_primop_alloc_value_in_callback_Test>::CreateTest()

looks like unsafe_new_with_self needs a specific order and EvalState takes the builder->fetchSettings and builder->settings as arguments.

@xokdvium

Copy link
Copy Markdown
Contributor

takes the builder->fetchSettings and builder->settings as arguments.

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.

@xokdvium

xokdvium commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

I guess this solution is also fine if it works with sanitizers... Doesn't work for plugins.

Comment thread src/libexpr-c/nix_api_external.cc Outdated
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)?

@Mic92 Mic92 force-pushed the fix-eval-state-in-c-primops branch 2 times, most recently from e57e9bb to a6922cc Compare February 19, 2026 16:12
@xokdvium

Copy link
Copy Markdown
Contributor

It's still unclear to me how a C plugin registering a builtin via nix_register_primop could receive a valid instance of EvalState. Seems like even in case the nix::EvalState is created by the C++ code it needs something to pass back to a C plugin.

@Mic92 Mic92 force-pushed the fix-eval-state-in-c-primops branch from a6922cc to 2dc31e2 Compare February 19, 2026 16:18
@Mic92

Mic92 commented Feb 19, 2026

Copy link
Copy Markdown
Member Author

It's still unclear to me how a C plugin registering a builtin via nix_register_primop could receive a valid instance of EvalState. Seems like even in case the nix::EvalState is created by the C++ code it needs something to pass back to a C plugin.

Don't know how to solve this either...

@xokdvium

Copy link
Copy Markdown
Contributor

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.

@Mic92 Mic92 force-pushed the fix-eval-state-in-c-primops branch 3 times, most recently from bf2051a to e2c117a Compare March 2, 2026 12:58
@xokdvium

xokdvium commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

@Mic92

Mic92 commented Mar 2, 2026

Copy link
Copy Markdown
Member Author

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.
@roberth

roberth commented Mar 2, 2026

Copy link
Copy Markdown
Member

I think #15383 does a bit better on the surprise factor - no implicit struct layout equivalence requirements.
::EvalState is opaque to C API callers, so let's use that to our advantage.

@Mic92

Mic92 commented Mar 2, 2026

Copy link
Copy Markdown
Member Author

This is a better solution to this: #15383

@Mic92 Mic92 closed this Mar 2, 2026
@Mic92 Mic92 deleted the fix-eval-state-in-c-primops branch March 2, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c api Nix as a C library with a stable interface documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants