Skip to content

ffi: setFloat32()/setFloat64() coerce their value and discard a pending exception #65341

Description

@soulee-dev

Version

main (ad7a5b8)

Platform

Darwin 25.5.0 Darwin Kernel Version 25.5.0: Tue Jun  9 22:28:34 PDT 2026; root:xnu-12377.121.10~1/RELEASE_ARM64_T6041 arm64

Subsystem

ffi

What steps will reproduce the bug?

// node --experimental-ffi repro.js
const { getRawPointer, setFloat64, getFloat64 } = require('node:ffi');

const ptr = getRawPointer(Buffer.alloc(8));

for (const value of ['1.5', true, {}, null]) {
  setFloat64(ptr, 0, value);
  console.log(`setFloat64(${String(value)}) ->`, getFloat64(ptr, 0));
}

try {
  setFloat64(ptr, 0, { valueOf() { throw new RangeError('boom'); } });
} catch (err) {
  console.log('valueOf threw ->', err.name, err.message);
}

How often does it reproduce? Is there a required condition?

Always. setFloat32() behaves the same way.

What is the expected behavior? Why is that the expected behavior?

setFloat32() and setFloat64() should reject a value that is not a number, like every other setter in the module, and they should not replace an exception the value itself threw.

The same double is type-checked on the argument path, where ToFFIArgument() requires IsNumber() and throws Argument %s must be a double, and the documentation says the setters "validate the supplied JavaScript value against the target native type before writing it into memory".

What do you see instead?

setFloat64(1.5) -> 1.5
setFloat64(true) -> 1
setFloat64([object Object]) -> NaN
setFloat64(null) -> 0
valueOf threw -> TypeError Value must be a number

The integer branches of SetValue<T>() require IsNumber() before any range check, but the floating-point branch calls ToNumber() and casts the result. When ToNumber() itself fails, the ERR_INVALID_ARG_VALUE overwrites the exception it left pending, so the RangeError never reaches the caller. DataView.prototype.setFloat64() and Buffer.prototype.writeDoubleLE() both propagate it.

node/src/ffi/data.cc

Lines 402 to 411 in ad7a5b8

} else if constexpr (std::is_same_v<T, float> || std::is_same_v<T, double>) {
MaybeLocal<Number> number = value->ToNumber(context);
Local<Number> number_local;
if (!number.ToLocal(&number_local)) {
THROW_ERR_INVALID_ARG_VALUE(env, "Value must be a number");
return;
}
converted = static_cast<T>(number_local->Value());

Additional information

The setters write straight into native memory, so a '1,5' typo from a config file stores NaN in a struct field with no error at the call site. setInt32() throws on the same mistake.

DataView and Buffer coerce here too, but they coerce for integers as well (dv.setInt8(0, 300) writes 44), while node:ffi rejects those.

The integer validation, the float coercion and the documentation sentence all landed together in d0fa608, the floating-point branch has not been modified since, and I found no review comment about the difference in the pull requests that touched it.

Refs: #62072
Refs: #62762
Refs: #62858
Refs: #65342

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions