Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,8 @@
'src/ffi/data.h',
'src/ffi/fast.cc',
'src/ffi/fast.h',
'src/ffi/jit_memory.cc',
'src/ffi/jit_memory.h',
'src/ffi/types.cc',
'src/ffi/types.h',
],
Expand Down
26 changes: 26 additions & 0 deletions src/ffi/fast.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include "ffi/fast.h"

#include "env-inl.h"
#include "ffi/jit_memory.h"
#include "ffi/types.h"
#include "node_errors.h"
#include "node_ffi.h"

Expand Down Expand Up @@ -219,7 +221,31 @@ FastFFIMetadata::~FastFFIMetadata() {
node_ffi_free_fast_trampoline(&trampoline);
}

bool IsFastCallSupported() {
// Fast call requires both a platform stub emitter and working JIT memory.
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__x86_64__)
return IsJitMemorySupported();
#else
return false;
#endif
}

std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
// Bail early if executable memory allocation doesn't work on this process
// (missing MAP_JIT entitlement, hardened runtime, SELinux execmem, etc.).
// The self-test runs once and caches the result.
if (!IsJitMemorySupported()) {
return nullptr;
}

// Check signature-level eligibility (type checks, register caps, platform
// support). Returning nullptr here lets the caller fall back to SharedBuffer
// or the generic libffi path.
const char* eligibility_reason;
if (!IsFastCallEligible(fn, &eligibility_reason)) {
return nullptr;
}
Comment on lines +241 to +247

// Reject unsupported result types first. Returning nullptr means the caller
// can still fall back to SharedBuffer or the generic libffi path.
FastFFIType result;
Expand Down
8 changes: 8 additions & 0 deletions src/ffi/fast.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ struct FastFFIMetadata {
v8::CFunction c_function;
};

// Public detection queries.

// Returns true if the fast-call path is available at all on this process
// (platform stub emitter exists + JIT memory self-test passed). Independent
// of any particular signature — if this returns false, no signature can
// use the fast-call path.
bool IsFastCallSupported();

bool SignatureNeedsRawPointerConversions(const FFIFunction& fn);
bool IsPointerTypeName(const std::string& name);
bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn);
Expand Down
104 changes: 104 additions & 0 deletions src/ffi/jit_memory.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#if HAVE_FFI

#include "ffi/jit_memory.h"

#if !defined(_WIN32)

#include <sys/mman.h>
#include <unistd.h>
Comment thread
ShogunPanda marked this conversation as resolved.

#include <cstdint>
#include <cstring>
#include <mutex>

#if defined(__APPLE__)
#include <libkern/OSCacheControl.h>
#endif

#endif // !defined(_WIN32)

namespace node::ffi {

namespace {

#if !defined(_WIN32)

bool SelfTest() {
#if defined(__aarch64__) || defined(_M_ARM64)
// AArch64 BR LR: 0xD65F03C0
constexpr uint32_t kInstruction = 0xD65F03C0;
constexpr size_t kInstructionSize = sizeof(uint32_t);
#elif defined(__x86_64__)
// x86_64 RET: 0xC3
constexpr uint8_t kInstruction = 0xC3;
constexpr size_t kInstructionSize = sizeof(uint8_t);
#else
// No stub emitter for this platform; nothing to test.
return false;
#endif // __aarch64__ || _M_ARM64 || __x86_64__

const size_t page_size = static_cast<size_t>(getpagesize());
void* page = mmap(nullptr, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON, -1, 0);
if (page == MAP_FAILED) {
return false;
}

uint8_t* code = static_cast<uint8_t*>(page);
#if defined(__aarch64__) || defined(_M_ARM64)
std::memcpy(code, &kInstruction, kInstructionSize);
#elif defined(__x86_64__)
code[0] = kInstruction;
#endif

#if defined(__APPLE__)
sys_icache_invalidate(page, kInstructionSize);
#else
__builtin___clear_cache(static_cast<char*>(page),
static_cast<char*>(page) + kInstructionSize);
#endif

// Transition the page to RX. This is the operation that actually fails on
// the restricted environments we care about (macOS hardened runtime without
// MAP_JIT, SELinux execmem denial, other OS-level execmem policies), so a
// successful mprotect is the support signal.
//
// We deliberately do NOT execute the page. This probe may run during normal
// operation (e.g. when an FFI function is first created), and executing
// freshly generated code from a capability check could SIGSEGV/SIGKILL the
// whole process on systems that block it. The real trampoline emitter runs
// the same mprotect at creation time and falls back to libffi when it is
// rejected, so environments that deny PROT_EXEC outright stay guarded.
//
// This does NOT cover the rarer case where mprotect(PROT_EXEC) succeeds but
// execution itself still faults (e.g. macOS hardened runtime without
// MAP_JIT, which this code path does not request). We accept that residual
// risk rather than crash the process from a capability check; builds that
// need fast FFI ship with the appropriate JIT entitlements.
const bool ok = mprotect(page, page_size, PROT_READ | PROT_EXEC) == 0;
munmap(page, page_size);
return ok;
}

#endif // !defined(_WIN32)

} // namespace

bool IsJitMemorySupported() {
#if defined(_WIN32)
// Windows stub emitter and VirtualAlloc-based JIT memory support not yet
// implemented. Return false so the fast-call path falls back to libffi.
return false;
#else
// Run the self-test exactly once and publish only the final result, so
// concurrent callers never observe a provisional value.
static std::once_flag once;
static bool supported = false;
std::call_once(once, [] { supported = SelfTest(); });
return supported;
#endif
}

} // namespace node::ffi

#endif // HAVE_FFI
26 changes: 26 additions & 0 deletions src/ffi/jit_memory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

namespace node::ffi {

// Returns true if executable memory allocation (mmap + mprotect to RX) works
// on this process. Runs a one-time self-test that allocates a tiny stub,
// writes a ret-style instruction, and transitions it to RX. The page is not
// executed: a successful RX transition is the support signal, and executing a
// freshly generated probe could crash the process on systems that block it.
//
// Catches:
// - macOS MAP_JIT entitlement missing
// - Hardened-runtime restrictions
// - SELinux execmem denial
// - Other OS-level restrictions on executable memory
//
// The self-test runs exactly once (std::call_once) and the result is cached
// process-wide. Subsequent calls return the cached value without re-running
// the test.
bool IsJitMemorySupported();

} // namespace node::ffi

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
158 changes: 158 additions & 0 deletions src/ffi/types.cc
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,164 @@ bool SignaturesMatch(const FFIFunction& fn,
return true;
}

namespace {

bool IsFastCallEligibleFFIType(ffi_type* type) {
// Accept all numeric types, pointer, and void (void OK as return only).
// Rejects struct types (not yet supported in fast-call path).
return type == &ffi_type_void || type == &ffi_type_sint8 ||
type == &ffi_type_uint8 || type == &ffi_type_sint16 ||
type == &ffi_type_uint16 || type == &ffi_type_sint32 ||
type == &ffi_type_uint32 || type == &ffi_type_sint64 ||
type == &ffi_type_uint64 || type == &ffi_type_float ||
type == &ffi_type_double || type == &ffi_type_pointer;
}

bool IsFunctionTypeName(const std::string& name) {
return name == "function";
}

// Check if an FFI type occupies a floating-point register.
bool IsFFITypeFloat(ffi_type* type) {
return type == &ffi_type_float || type == &ffi_type_double;
}

// Check if an FFI type name maps to a kBuffer (kV8Value) argument in the
// fast-call path, which consumes an extra GP register for the helper call.
bool IsBufferTypeName(const std::string& name) {
return name == "buffer" || name == "arraybuffer";
}

} // namespace

bool IsFastCallEligible(const FFIFunction& fn, const char** out_reason) {
static const char* dummy = "";
if (out_reason == nullptr) out_reason = &dummy;

// Check that a platform stub emitter exists for the current ABI.
// Stub emitters cover AArch64 (Linux/macOS/FreeBSD/Windows) and
// x86_64 (SysV: Linux/macOS/FreeBSD, Win64: Windows). Other platforms
// fall back to libffi.
#if !defined(__aarch64__) && !defined(_M_ARM64) && !defined(__x86_64__)
*out_reason = "no platform stub emitter";
return false;
#endif

// Check return type eligibility.
if (!IsFastCallEligibleFFIType(fn.return_type)) {
*out_reason = "unsupported return type";
return false;
}
if (IsFunctionTypeName(fn.return_type_name)) {
*out_reason = "return type is function";
return false;
}

// V8's fast-call lowering caps the C-side arg count. With HasReceiver=kNo
// there's no implicit receiver in the count, so this is the user-arg cap.
// V8's hard limit is 8 args; signatures over that fall back to libffi.
if (fn.args.size() > 8) {
*out_reason = "argument count exceeds V8 fast-call cap";
return false;
}

// Per-ABI register caps for arguments that must be passed in registers.
// If an arg can't fit in a register, it goes on the stack — which the
// current trampoline generators don't support.
// `args` and `arg_type_names` are read in lockstep below. A malformed
// FFIFunction with mismatched lengths would otherwise index out of bounds,
// so reject it here rather than relying on callers to pre-validate.
if (fn.args.size() != fn.arg_type_names.size()) {
*out_reason = "argument type name count mismatch";
return false;
}

size_t gp_count = 0;
size_t fp_count = 0;
bool has_buffer_arg = false;
for (size_t i = 0; i < fn.args.size(); ++i) {
ffi_type* t = fn.args[i];
const std::string& name = fn.arg_type_names[i];

if (!IsFastCallEligibleFFIType(t)) {
*out_reason = "unsupported arg type";
return false;
}
// `void` is fine as a return type but has no register slot, so it cannot
// appear in `args`.
if (t == &ffi_type_void) {
*out_reason = "void cannot be an argument type";
return false;
}
if (IsFunctionTypeName(name)) {
*out_reason = "arg is function";
return false;
}

// `buffer`/`arraybuffer` args arrive as kV8Value in the V8 fast-call
// signature, consuming an extra GP register for the helper call.
if (IsBufferTypeName(name)) {
has_buffer_arg = true;
}

// Count register classes used by each argument.
if (IsFFITypeFloat(t)) {
fp_count++;
} else {
gp_count++;
}
}

// Platform-specific register pressure limits.
#if defined(__aarch64__) || defined(_M_ARM64)
// AArch64: 8 FP registers (v0-v7) + up to 7 GP registers per trampoline
// constraint (the 8th GP slot is consumed by the helper call for buffer
// args). Buffer args and float args can't coexist — the helper call would
// clobber FP state.
const size_t effective_gp = gp_count + (has_buffer_arg ? 1 : 0);
if (has_buffer_arg && fp_count != 0) {
*out_reason = "buffer and float args cannot coexist on AArch64";
return false;
}
if (effective_gp > 7 || fp_count > 8) {
*out_reason = "argument count exceeds AArch64 register limit";
return false;
}
#elif defined(__x86_64__)
#if defined(_WIN32)
// No Win64 trampoline emitter exists (src/ffi/platforms implements only
// AArch64 and x86_64 SysV), so Win64 fast-call is never eligible. This is
// already short-circuited earlier by IsJitMemorySupported() returning false
// on Windows; rejecting here keeps eligibility self-consistent regardless of
// caller order.
*out_reason = "no Win64 fast-call trampoline emitter";
return false;
#else
// x86_64 SysV: the V8 receiver occupies rdi, leaving rsi, rdx, rcx, r8, r9
// (5 incoming GP slots); scalar signatures can load one more user GP arg
// from the caller stack, for an effective cap of 6 GP. FP args use
// xmm0-xmm7. Buffer args spill the whole incoming GP window through a C++
// helper, so they cannot coexist with FP args and stay register-only
// (incoming GP = gp + 1 must fit in the 5 incoming registers, i.e. <= 4 GP
// when a buffer is present). These rules mirror the constraints enforced by
// node_ffi_create_fast_trampoline in src/ffi/platforms/x64.cc.
if (has_buffer_arg && fp_count != 0) {
*out_reason = "buffer and float args cannot coexist on x86_64 SysV";
return false;
}
const size_t incoming_gp = gp_count + (has_buffer_arg ? 1 : 0);
const size_t max_incoming_gp = has_buffer_arg ? 5 : 6;
if (incoming_gp > max_incoming_gp || fp_count > 8) {
*out_reason = "argument count exceeds x86_64 SysV register limit";
return false;
}
#endif // _WIN32
Comment thread
ShogunPanda marked this conversation as resolved.
#endif // __x86_64__

*out_reason = "";
return true;
}

bool IsSBEligibleFFIType(ffi_type* type) {
return type == &ffi_type_void || type == &ffi_type_sint8 ||
type == &ffi_type_uint8 || type == &ffi_type_sint16 ||
Expand Down
14 changes: 14 additions & 0 deletions src/ffi/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ bool SignaturesMatch(const FFIFunction& fn,
ffi_type* return_type,
const std::vector<ffi_type*>& args);

// Returns true if `fn` can be invoked via the V8 fast-call path. On
// false, `*out_reason` is set to a static string describing why
// (never null after this returns; callers may pass nullptr to ignore).
//
// Eligibility checks: every arg type and the return type are
// numeric-or-pointer, no `function`-typed args/return, arg count
// within V8 fast-call cap (8), and register-passed arg counts within
// per-ABI limits. Trampoline emitters currently exist only for AArch64
// (≤ 7 GP + ≤ 8 FP) and x86_64 SysV (≤ 6 GP + ≤ 8 FP; buffer args cap GP at
// 5 and cannot coexist with FP args). Platforms without an emitter
// (including Win64) are reported ineligible so the caller falls back to
// libffi.
bool IsFastCallEligible(const FFIFunction& fn, const char** out_reason);

// True if the FFI type can be read from / written to a raw byte buffer
// without needing V8 operations (conversion, allocation, etc.).
bool IsSBEligibleFFIType(ffi_type* type);
Expand Down
Loading