fix: gate ErlNifResourceTypeInit members/dyncall on nif_version_2_16 to fix OTP 22/23 heap overflow#725
Conversation
On NIF versions < 2.16 (OTP < 24), ErlNifResourceTypeInit only has three fields (dtor, stop, down = 24 bytes). The fields `members` and `dyncall` were added in NIF 2.16 / OTP 24. Since PR rusterlium#358 (May 2021) these two fields were added to the Rust struct unconditionally, making it 40 bytes on all NIF versions. When running on OTP 22 or 23, `enif_open_resource_type_x` calls: sys_memcpy(&ort->new_callbacks, init, sizeof(ErlNifResourceTypeInit)) where OTP's own sizeof is 24 bytes, but it copies 40 bytes from the NIF's struct — a 16-byte heap overflow into the next allocation. On OTP debug builds this triggers an ASSERT immediately. On release builds it causes silent, timing-dependent heap corruption with a ~35-40% crash rate. The fix gates both fields on `#[cfg(feature = "nif_version_2_16")]` so the struct size matches what OTP expects for each NIF API level. Rustler's own README documents NIF 2.15 (OTP 22) as the default, so this version must work without heap corruption. Companion struct-literal initialisers in resource/registration.rs are gated accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thank you for the fix. OTP's behaviour here is really odd :) The comment is also a bit off. The behaviour is only broken in OTP<24. From then onwards (erlang/otp#4741) the members are copied individually instead of "blindly" memcpy'ing everything over. |
Well, thank you! That was fast. |
Summary
Gates
ErlNifResourceTypeInit.membersandErlNifResourceTypeInit.dyncallon#[cfg(feature = "nif_version_2_16")]to fix a heap overflow when running on OTP 22/23.Root cause
On NIF API versions < 2.16 (OTP 22 and 23),
ErlNifResourceTypeInitonly has three fields:The fields
membersanddyncallwere added in NIF 2.16 / OTP 24. Since PR #358 (May 2021), Rustler has included both fields in the Rust struct unconditionally, making it 40 bytes regardless of the active NIF version.When
enif_open_resource_type_xis called on OTP 22/23, it does:where OTP's own
sizeofis 24 bytes, butinitpoints to Rustler's 40-byte struct, so OTP reads 16 bytes past the end of thenew_callbacksfield, overflowing into the next heap allocation.ASSERTfires immediately on startup.Fix
Gate both fields on
#[cfg(feature = "nif_version_2_16")]inrustler/src/sys/types.rsand update the companion struct-literal initialisers inrustler/src/resource/registration.rsaccordingly.The struct is then 24 bytes on NIF < 2.16 and 40 bytes on NIF >= 2.16, matching what OTP expects.
Why this matters
Rustler's own README documents NIF 2.15 (OTP 22) as the default supported version. The bug makes every NIF compiled with default settings silently corrupt heap on OTP 22/23.