infix
A JIT-Powered FFI Library for C
Loading...
Searching...
No Matches
executor.c
Go to the documentation of this file.
1
38#include "common/utility.h"
39#include <stdio.h>
40#include <stdlib.h>
41#include <string.h>
42#include <time.h>
43// Platform-Specific Includes
44#if defined(INFIX_OS_WINDOWS)
45#include <windows.h>
46#else
47#include <errno.h>
48#include <fcntl.h>
49#include <sys/mman.h>
50#include <sys/types.h>
51#include <unistd.h>
52#endif
53#if defined(INFIX_OS_MACOS)
54#include <dlfcn.h>
55#include <pthread.h>
56#endif
57// Polyfills for mmap flags for maximum POSIX compatibility.
58#if defined(INFIX_ENV_POSIX) && !defined(INFIX_OS_WINDOWS)
59#if !defined(MAP_ANON) && defined(MAP_ANONYMOUS)
60#define MAP_ANON MAP_ANONYMOUS
61#endif
62#endif
63// macOS JIT Security Hardening Logic
64#if defined(INFIX_OS_MACOS)
79typedef const struct __CFString * CFStringRef;
80typedef const void * CFTypeRef;
81typedef struct __SecTask * SecTaskRef;
82typedef struct __CFError * CFErrorRef;
83#define kCFStringEncodingUTF8 0x08000100
84// A struct to hold dynamically loaded function pointers from macOS frameworks.
85static struct {
86 void (*CFRelease)(CFTypeRef);
87 bool (*CFBooleanGetValue)(CFTypeRef boolean);
88 CFStringRef (*CFStringCreateWithCString)(CFTypeRef allocator, const char * cStr, uint32_t encoding);
89 CFTypeRef kCFAllocatorDefault;
90 SecTaskRef (*SecTaskCreateFromSelf)(CFTypeRef allocator);
91 CFTypeRef (*SecTaskCopyValueForEntitlement)(SecTaskRef task, CFStringRef entitlement, CFErrorRef * error);
92} g_macos_apis;
100static void initialize_macos_apis(void) {
101 // We don't need to link against these frameworks, which makes building simpler.
102 void * cf = dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", RTLD_LAZY);
103 void * sec = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY);
104 if (!cf || !sec) {
105 INFIX_DEBUG_PRINTF("Warning: Could not dlopen macOS frameworks. JIT security features will be degraded.");
106 if (cf)
107 dlclose(cf);
108 if (sec)
109 dlclose(sec);
110 memset(&g_macos_apis, 0, sizeof(g_macos_apis));
111 return;
112 }
113 g_macos_apis.CFRelease = dlsym(cf, "CFRelease");
114 g_macos_apis.CFBooleanGetValue = dlsym(cf, "CFBooleanGetValue");
115 g_macos_apis.CFStringCreateWithCString = dlsym(cf, "CFStringCreateWithCString");
116 void ** pAlloc = (void **)dlsym(cf, "kCFAllocatorDefault");
117 if (pAlloc)
118 g_macos_apis.kCFAllocatorDefault = *pAlloc;
119 g_macos_apis.SecTaskCreateFromSelf = dlsym(sec, "SecTaskCreateFromSelf");
120 g_macos_apis.SecTaskCopyValueForEntitlement = dlsym(sec, "SecTaskCopyValueForEntitlement");
121 dlclose(cf);
122 dlclose(sec);
123}
129static bool has_jit_entitlement(void) {
130 // Use pthread_once to ensure the dynamic loading happens exactly once, thread-safely.
131 static pthread_once_t init_once = PTHREAD_ONCE_INIT;
132 pthread_once(&init_once, initialize_macos_apis);
133 if (!g_macos_apis.SecTaskCopyValueForEntitlement || !g_macos_apis.CFStringCreateWithCString)
134 return false;
135 bool result = false;
136 SecTaskRef task = g_macos_apis.SecTaskCreateFromSelf(g_macos_apis.kCFAllocatorDefault);
137 if (!task)
138 return false;
139 CFStringRef key = g_macos_apis.CFStringCreateWithCString(
140 g_macos_apis.kCFAllocatorDefault, "com.apple.security.cs.allow-jit", kCFStringEncodingUTF8);
141 CFTypeRef value = nullptr;
142 if (key) {
143 // This is the core check: ask the system for the value of the entitlement.
144 value = g_macos_apis.SecTaskCopyValueForEntitlement(task, key, nullptr);
145 g_macos_apis.CFRelease(key);
146 }
147 g_macos_apis.CFRelease(task);
148 if (value) {
149 // The value of the entitlement is a CFBoolean, so we must extract its value.
150 if (g_macos_apis.CFBooleanGetValue && g_macos_apis.CFBooleanGetValue(value))
151 result = true;
152 g_macos_apis.CFRelease(value);
153 }
154 return result;
155}
156#endif // INFIX_OS_MACOS
157// Hardened POSIX Anonymous Shared Memory Allocator (for Dual-Mapping W^X)
158#if !defined(INFIX_OS_WINDOWS) && !defined(INFIX_OS_MACOS) && !defined(INFIX_OS_ANDROID) && !defined(INFIX_OS_OPENBSD)
159#include <fcntl.h>
160#include <stdint.h>
173static int shm_open_anonymous() {
174 char shm_name[64];
175 uint64_t random_val = 0;
176 // Generate a sufficiently random name to avoid collisions if multiple processes
177 // are running this code simultaneously. Using /dev/urandom is a robust way to do this.
178 int rand_fd = open("/dev/urandom", O_RDONLY);
179 if (rand_fd < 0)
180 return -1;
181 ssize_t bytes_read = read(rand_fd, &random_val, sizeof(random_val));
182 close(rand_fd);
183 if (bytes_read != sizeof(random_val))
184 return -1;
185 snprintf(shm_name, sizeof(shm_name), "/infix-jit-%d-%llx", getpid(), (unsigned long long)random_val);
186 // Create the shared memory object exclusively.
187 int fd = shm_open(shm_name, O_RDWR | O_CREAT | O_EXCL, 0600);
188 if (fd >= 0) {
189 // Unlink immediately. The file descriptor remains valid, but the name is removed.
190 // This ensures the kernel will clean up the memory object when the last fd is closed.
191 shm_unlink(shm_name);
192 return fd;
193 }
194 return -1;
195}
196#endif
197// Public API: Executable Memory Management
206#if defined(INFIX_OS_WINDOWS)
207 infix_executable_t exec = {.rx_ptr = nullptr, .rw_ptr = nullptr, .size = 0, .handle = nullptr};
208#else
209 infix_executable_t exec = {.rx_ptr = nullptr, .rw_ptr = nullptr, .size = 0, .shm_fd = -1};
210#endif
211 if (size == 0)
212 return exec;
213#if defined(INFIX_OS_WINDOWS)
214 // Windows: Single-mapping W^X. Allocate as RW, later change to RX via VirtualProtect.
215 void * code = VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
216 if (code == nullptr)
217 return exec;
218 exec.rw_ptr = code;
219 exec.rx_ptr = code;
220#elif defined(INFIX_OS_MACOS) || defined(INFIX_OS_ANDROID) || defined(INFIX_OS_OPENBSD) || defined(INFIX_OS_DRAGONFLY)
221 // Single-mapping POSIX platforms. Allocate as RW, later change to RX via mprotect.
222 void * code = MAP_FAILED;
223#if defined(MAP_ANON)
224 int flags = MAP_PRIVATE | MAP_ANON;
225#if defined(INFIX_OS_MACOS)
226 // On macOS, we perform a one-time check for JIT support.
227 static bool g_use_secure_jit_path = false;
228 static bool g_checked_jit_support = false;
229 if (!g_checked_jit_support) {
230 g_use_secure_jit_path = has_jit_entitlement();
231 INFIX_DEBUG_PRINTF("macOS JIT check: Entitlement found = %s. Using %s API.",
232 g_use_secure_jit_path ? "yes" : "no",
233 g_use_secure_jit_path ? "secure (MAP_JIT)" : "legacy (mprotect)");
234 g_checked_jit_support = true;
235 }
236 // If entitled, use the modern, more secure MAP_JIT flag.
237 if (g_use_secure_jit_path)
238 flags |= MAP_JIT;
239#endif // INFIX_OS_MACOS
240 code = mmap(nullptr, size, PROT_READ | PROT_WRITE, flags, -1, 0);
241#endif // MAP_ANON
242 if (code == MAP_FAILED) { // Fallback for older systems without MAP_ANON
243 int fd = open("/dev/zero", O_RDWR);
244 if (fd != -1) {
245 code = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
246 close(fd);
247 }
248 }
249 if (code == MAP_FAILED)
250 return exec;
251 exec.rw_ptr = code;
252 exec.rx_ptr = code;
253#else
254 // Dual-mapping POSIX platforms (e.g., Linux, FreeBSD). Create two separate views of the same memory.
255 exec.shm_fd = shm_open_anonymous();
256 if (exec.shm_fd < 0)
257 return exec;
258 if (ftruncate(exec.shm_fd, size) != 0) {
259 close(exec.shm_fd);
260 return exec;
261 }
262 // The RW mapping.
263 exec.rw_ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, exec.shm_fd, 0);
264 // The RX mapping of the exact same physical memory.
265 exec.rx_ptr = mmap(nullptr, size, PROT_READ | PROT_EXEC, MAP_SHARED, exec.shm_fd, 0);
266 // If either mapping fails, clean up both and return an error.
267 if (exec.rw_ptr == MAP_FAILED || exec.rx_ptr == MAP_FAILED) {
268 if (exec.rw_ptr != MAP_FAILED)
269 munmap(exec.rw_ptr, size);
270 if (exec.rx_ptr != MAP_FAILED)
271 munmap(exec.rx_ptr, size);
272 close(exec.shm_fd);
273 return (infix_executable_t){.rx_ptr = nullptr, .rw_ptr = nullptr, .size = 0, .shm_fd = -1};
274 }
275#endif
276 exec.size = size;
277 INFIX_DEBUG_PRINTF("Allocated JIT memory. RW at %p, RX at %p", exec.rw_ptr, exec.rx_ptr);
278 return exec;
279}
293 if (exec.size == 0)
294 return;
295#if defined(INFIX_OS_WINDOWS)
296 if (exec.rw_ptr) {
297 // Change protection to NOACCESS to catch use-after-free bugs immediately.
298 if (!VirtualProtect(exec.rw_ptr, exec.size, PAGE_NOACCESS, &(DWORD){0}))
299 INFIX_DEBUG_PRINTF("WARNING: VirtualProtect failed to set PAGE_NOACCESS guard page.");
300 VirtualFree(exec.rw_ptr, 0, MEM_RELEASE);
301 }
302#elif defined(INFIX_OS_MACOS)
303 // On macOS with MAP_JIT, the memory is managed with special thread-local permissions.
304 // We only need to unmap the single mapping.
305 if (exec.rw_ptr) {
306#if INFIX_MACOS_SECURE_JIT_AVAILABLE // This macro is not yet defined, placeholder for future
307 // If using the secure path, we should toggle write protection back on.
308 static bool g_use_secure_jit_path = false; // Re-check or use a shared flag
309 if (g_use_secure_jit_path)
310 pthread_jit_write_protect_np(true);
311#endif
312 // Creating a guard page before unmapping is good practice.
313 mprotect(exec.rw_ptr, exec.size, PROT_NONE);
314 munmap(exec.rw_ptr, exec.size);
315 }
316#elif defined(INFIX_OS_ANDROID) || defined(INFIX_OS_OPENBSD) || defined(INFIX_OS_DRAGONFLY)
317 // Other single-mapping POSIX systems.
318 if (exec.rw_ptr) {
319 mprotect(exec.rw_ptr, exec.size, PROT_NONE);
320 munmap(exec.rw_ptr, exec.size);
321 }
322#else
323 // Dual-mapping POSIX: protect and unmap both views.
324 if (exec.rx_ptr)
325 mprotect(exec.rx_ptr, exec.size, PROT_NONE);
326 if (exec.rw_ptr)
327 munmap(exec.rw_ptr, exec.size);
328 if (exec.rx_ptr && exec.rx_ptr != exec.rw_ptr) // rw_ptr might be same as rx_ptr on some platforms
329 munmap(exec.rx_ptr, exec.size);
330 if (exec.shm_fd >= 0)
331 close(exec.shm_fd);
332#endif
333}
351 if (exec.rw_ptr == nullptr || exec.size == 0)
352 return false;
353 // On AArch64 (and other RISC architectures), the instruction and data caches can be
354 // separate. We must explicitly flush the D-cache (where the JIT wrote the code)
355 // and invalidate the I-cache so the CPU fetches the new instructions.
356#if defined(INFIX_ARCH_AARCH64)
357#if defined(_MSC_VER)
358 // Use the Windows-specific API.
359 FlushInstructionCache(GetCurrentProcess(), exec.rw_ptr, exec.size);
360#else
361 // Use the GCC/Clang built-in for other platforms.
362 __builtin___clear_cache((char *)exec.rw_ptr, (char *)exec.rw_ptr + exec.size);
363#endif
364#endif
365 bool result = false;
366#if defined(INFIX_OS_WINDOWS)
367 // Finalize permissions to Read+Execute.
368 result = VirtualProtect(exec.rw_ptr, exec.size, PAGE_EXECUTE_READ, &(DWORD){0});
369#elif defined(INFIX_OS_MACOS)
370#if INFIX_MACOS_SECURE_JIT_AVAILABLE // Placeholder
371 static bool g_use_secure_jit_path = false;
372 if (g_use_secure_jit_path) {
373 pthread_jit_write_protect_np(false); // Make writable region executable.
374 result = true;
375 }
376 else
377#endif
378 // On macOS with the JIT entitlement, we don't use mprotect. Instead, we toggle
379 // a thread-local "write permission" state for all JIT memory. The memory is
380 // RX by default, and we temporarily make it RW for writing.
381 // However, the current logic does this change via `pthread_jit_write_protect_np`
382 // within the allocator itself. For now, this is a placeholder for that logic.
383 result = (mprotect(exec.rw_ptr, exec.size, PROT_READ | PROT_EXEC) == 0);
384#elif defined(INFIX_OS_ANDROID) || defined(INFIX_OS_OPENBSD) || defined(INFIX_OS_DRAGONFLY)
385 // Other single-mapping POSIX platforms use mprotect.
386 result = (mprotect(exec.rw_ptr, exec.size, PROT_READ | PROT_EXEC) == 0);
387#else
388 // On dual-mapping platforms, the RX mapping is already executable. This is a no-op.
389 result = true;
390#endif
391 if (result)
392 INFIX_DEBUG_PRINTF("Memory at %p is now executable.", exec.rx_ptr);
393 return result;
394}
395// Public API: Protected (Read-Only) Memory
408 infix_protected_t prot = {.rw_ptr = nullptr, .size = 0};
409 if (size == 0)
410 return prot;
411#if defined(INFIX_OS_WINDOWS)
412 prot.rw_ptr = VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
413#else
414#if defined(MAP_ANON)
415 prot.rw_ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
416#else
417 int fd = open("/dev/zero", O_RDWR);
418 if (fd == -1)
419 prot.rw_ptr = MAP_FAILED;
420 else {
421 prot.rw_ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
422 close(fd);
423 }
424#endif
425 if (prot.rw_ptr == MAP_FAILED)
426 prot.rw_ptr = nullptr;
427#endif
428 if (prot.rw_ptr)
429 prot.size = size;
430 return prot;
431}
438 if (prot.size == 0)
439 return;
440#if defined(INFIX_OS_WINDOWS)
441 VirtualFree(prot.rw_ptr, 0, MEM_RELEASE);
442#else
443 munmap(prot.rw_ptr, prot.size);
444#endif
445}
458 if (prot.size == 0)
459 return false;
460 bool result = false;
461#if defined(INFIX_OS_WINDOWS)
462 result = VirtualProtect(prot.rw_ptr, prot.size, PAGE_READONLY, &(DWORD){0});
463#else
464 result = (mprotect(prot.rw_ptr, prot.size, PROT_READ) == 0);
465#endif
466 return result;
467}
468// Universal Reverse Call Dispatcher
492void infix_internal_dispatch_callback_fn_impl(infix_reverse_t * context, void * return_value_ptr, void ** args_array) {
494 "Dispatching reverse call. Context: %p, User Fn: %p", (void *)context, context->user_callback_fn);
495 if (context->user_callback_fn == nullptr) {
496 // If no handler is set, do nothing. If the function has a return value,
497 // it's good practice to zero it out to avoid returning garbage.
498 if (return_value_ptr && context->return_type->size > 0)
499 infix_memset(return_value_ptr, 0, context->return_type->size);
500 return;
501 }
502 if (context->cached_forward_trampoline != nullptr) {
503 // Path 1: Type-safe "callback". Use the pre-generated forward trampoline to
504 // call the user's C function with the correct signature. This is efficient
505 // and provides a clean interface for the C developer.
507 cif_func(return_value_ptr, args_array);
508 }
509 else {
510 // Path 2: Generic "closure". Directly call the user's generic handler.
511 // This path is more flexible and is intended for language bindings where the
512 // handler needs access to the context and raw argument pointers.
514 handler(context, return_value_ptr, args_array);
515 }
516 INFIX_DEBUG_PRINTF("Exiting reverse call dispatcher.");
517}
#define c23_nodiscard
A compatibility macro for the C23 [[nodiscard]] attribute.
Definition compat_c23.h:106
void infix_protected_free(infix_protected_t prot)
Frees a block of protected memory.
Definition executor.c:437
c23_nodiscard bool infix_executable_make_executable(infix_executable_t exec)
Makes a block of JIT memory executable, completing the W^X process.
Definition executor.c:350
void infix_executable_free(infix_executable_t exec)
Frees a block of executable memory and applies guard pages to prevent use-after-free.
Definition executor.c:292
c23_nodiscard infix_protected_t infix_protected_alloc(size_t size)
Allocates a block of standard memory for later protection.
Definition executor.c:407
c23_nodiscard infix_executable_t infix_executable_alloc(size_t size)
Allocates a block of executable memory using the platform's W^X strategy.
Definition executor.c:205
c23_nodiscard bool infix_protected_make_readonly(infix_protected_t prot)
Makes a block of memory read-only for security hardening.
Definition executor.c:457
static int shm_open_anonymous()
Definition executor.c:173
void infix_internal_dispatch_callback_fn_impl(infix_reverse_t *context, void *return_value_ptr, void **args_array)
The universal C entry point for all reverse call trampolines.
Definition executor.c:492
void(* infix_cif_func)(void *, void **)
A function pointer type for a bound forward trampoline.
Definition infix.h:336
size_t size
Definition infix.h:197
void(* infix_closure_handler_fn)(infix_context_t *, void *, void **)
A function pointer type for a generic closure handler.
Definition infix.h:348
c23_nodiscard infix_cif_func infix_forward_get_code(infix_forward_t *)
Gets the callable function pointer from a bound forward trampoline.
Definition trampoline.c:278
#define infix_memset
A macro that can be defined to override the default memset function.
Definition infix.h:309
Internal data structures, function prototypes, and constants.
Internal representation of an executable memory block for JIT code.
Definition infix_internals.h:56
size_t size
Definition infix_internals.h:64
void * rw_ptr
Definition infix_internals.h:63
void * rx_ptr
Definition infix_internals.h:62
int shm_fd
Definition infix_internals.h:60
Internal representation of a memory block that will be made read-only.
Definition infix_internals.h:75
size_t size
Definition infix_internals.h:77
void * rw_ptr
Definition infix_internals.h:76
Internal definition of a reverse trampoline (callback/closure) handle.
Definition infix_internals.h:114
infix_type * return_type
Definition infix_internals.h:118
void * user_callback_fn
Definition infix_internals.h:123
infix_forward_t * cached_forward_trampoline
Definition infix_internals.h:128
A header for conditionally compiled debugging utilities.
#define INFIX_DEBUG_PRINTF(...)
Definition utility.h:100