From 52b2bf580b8806991f045c29224ed8f2eb4ad4fa Mon Sep 17 00:00:00 2001 From: FBarrca Date: Tue, 3 Nov 2026 13:53:32 +0100 Subject: [PATCH 1/2] Add macOS port for arm64 (Apple Silicon) and x86-64 New ports under src/port/aarch64/macos and src/port/x86-64/macos: - Context switch: AArch64 assembly saving X19-X30, D8-D15, FPCR/FPSR, and a Mach-O (macho64) variant of the x86-64 NASM switch. - Guarded coroutine stacks via posix_memalign + mprotect, main stack bounds from pthread_get_stackaddr_np / pthread_get_stacksize_np. - Seeds from arc4random_buf, core count from sysconf. Shared changes, all limited to the new platforms: - cmi_config.h recognises arm64 and macOS. - Trial recovery uses setjmp/longjmp on arm64, since Clang does not implement __builtin_setjmp on AArch64. - Build: dispatch to the new ports, only require NASM on x86-64, use _DARWIN_C_SOURCE instead of _POSIX_C_SOURCE on macOS, and only pass -ffat-lto-objects when the compiler accepts it (Apple Clang does not). Co-Authored-By: Claude Includes-AI-Code: true --- meson.build | 15 +- src/cimba.c | 8 + src/cmi_config.h | 15 +- src/meson.build | 3 +- src/port/aarch64/macos/cmb_random_hwseed.c | 19 +++ .../aarch64/macos/cmi_coroutine_context.S | 63 ++++++++ .../aarch64/macos/cmi_coroutine_context.c | 128 +++++++++++++++ src/port/aarch64/macos/cmi_cpu_cores.c | 15 ++ src/port/aarch64/macos/cmi_memutils.c | 46 ++++++ src/port/aarch64/macos/meson.build | 7 + src/port/meson.build | 11 +- src/port/x86-64/macos/cmb_random_hwseed.c | 19 +++ .../x86-64/macos/cmi_coroutine_context.asm | 59 +++++++ src/port/x86-64/macos/cmi_coroutine_context.c | 146 ++++++++++++++++++ src/port/x86-64/macos/cmi_cpu_cores.c | 15 ++ src/port/x86-64/macos/cmi_memutils.c | 46 ++++++ src/port/x86-64/macos/meson.build | 13 ++ src/port/x86-64/meson.build | 2 + 18 files changed, 620 insertions(+), 10 deletions(-) create mode 100644 src/port/aarch64/macos/cmb_random_hwseed.c create mode 100644 src/port/aarch64/macos/cmi_coroutine_context.S create mode 100644 src/port/aarch64/macos/cmi_coroutine_context.c create mode 100644 src/port/aarch64/macos/cmi_cpu_cores.c create mode 100644 src/port/aarch64/macos/cmi_memutils.c create mode 100644 src/port/aarch64/macos/meson.build create mode 100644 src/port/x86-64/macos/cmb_random_hwseed.c create mode 100644 src/port/x86-64/macos/cmi_coroutine_context.asm create mode 100644 src/port/x86-64/macos/cmi_coroutine_context.c create mode 100644 src/port/x86-64/macos/cmi_cpu_cores.c create mode 100644 src/port/x86-64/macos/cmi_memutils.c create mode 100644 src/port/x86-64/macos/meson.build diff --git a/meson.build b/meson.build index 7bd86e2f..ad8db781 100644 --- a/meson.build +++ b/meson.build @@ -11,8 +11,10 @@ project('cimba', ['c'], ] ) -# We also need the portable Netwide Assembler for assembly code -add_languages('nasm', required : true, native : true) +# We also need the portable Netwide Assembler for the x86-64 assembly code +if host_machine.cpu_family() == 'x86_64' + add_languages('nasm', required : true, native : true) +endif # Try to find CUDA, but don't fail if it's missing add_languages('cuda', required : false, native : true) @@ -33,11 +35,18 @@ cc = meson.get_compiler('c') # Flags common to all build types common_flags = [ '-Wno-pedantic', - '-D_POSIX_C_SOURCE=200809L', '-fno-semantic-interposition', '-ffp-contract=off', ] +if host_machine.system() == 'darwin' + # Keep Darwin extensions such as pthread_get_stackaddr_np() and + # arc4random_buf() visible + common_flags += '-D_DARWIN_C_SOURCE' +else + common_flags += '-D_POSIX_C_SOURCE=200809L' +endif + # Flags per build type # 'debug' : full asserts and logging, no optimization # 'release' : standard optimized build, release asserts only diff --git a/src/cimba.c b/src/cimba.c index ccc28e8d..6b860461 100644 --- a/src/cimba.c +++ b/src/cimba.c @@ -23,6 +23,7 @@ #include #include +#include #include #include "cimba.h" @@ -75,9 +76,16 @@ static uint64_t leaking_trials; static CMB_THREAD_LOCAL cimba_trial_cleanup_func *trial_cleanup_func = NULL; static CMB_THREAD_LOCAL void *trial_cleanup_arg = NULL; +#if CMI_ARCH == CMI_ARM64 +/* Clang does not implement __builtin_setjmp on AArch64, use the libc version */ +static CMB_THREAD_LOCAL jmp_buf recovery_buf; +#define recovery_set() setjmp(recovery_buf) +#define recovery_jump() longjmp(recovery_buf, 1) +#else static CMB_THREAD_LOCAL intptr_t recovery_buf[5]; #define recovery_set() __builtin_setjmp((void **)recovery_buf) #define recovery_jump() __builtin_longjmp((void **)recovery_buf, 1) +#endif extern void cmi_coroutine_recovery_prepare(void *stack_marker); extern void cmi_coroutine_recovery_finalize(void *stack_marker); diff --git a/src/cmi_config.h b/src/cmi_config.h index 0998cc6b..25c7933e 100644 --- a/src/cmi_config.h +++ b/src/cmi_config.h @@ -23,8 +23,8 @@ /* * Supported toolchains: - * - Architecture: x86-64 / AMD64 - * - Operating system: Linux, or Windows via MinGW-w64 + * - Architecture: x86-64 / AMD64, or arm64 (Apple Silicon) + * - Operating system: Linux, Windows via MinGW-w64, or macOS * - Compiler: GCC or Clang, C11 or later * (the library itself is built as C23) * @@ -32,25 +32,32 @@ */ /* - * Identify the processor architecture. So far, only AMD64/x86-64 is supported. + * Identify the processor architecture. AMD64/x86-64 everywhere, and arm64 on + * macOS (Apple Silicon). */ #define CMI_AMD64 1 +#define CMI_ARM64 2 #if (defined (__amd64__) || defined (__amd64) || defined (__x86_64__) || \ defined (__x86_64) || defined (_M_X64) || defined (_M_AMD64)) #define CMI_ARCH CMI_AMD64 +#elif defined (__aarch64__) || defined (__arm64__) + #define CMI_ARCH CMI_ARM64 #else #error "Platform architecture not yet supported." #endif /* - * Identify the operating system. So far, only Linux and Windows are supported. + * Identify the operating system. Linux, Windows, and macOS are supported. */ #define CMI_LINUX 1 #define CMI_WINDOWS 2 +#define CMI_MACOS 3 #if defined (__linux__) || defined (__linux) || defined (linux) #define CMI_OS CMI_LINUX #elif defined (_WIN64) || defined (_WIN32) || defined (__WIN32__) #define CMI_OS CMI_WINDOWS +#elif defined (__APPLE__) && defined (__MACH__) + #define CMI_OS CMI_MACOS #else #error "Platform operating system not yet supported." #endif diff --git a/src/meson.build b/src/meson.build index 15958bb9..daec7d7d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -66,7 +66,8 @@ if host_machine.system() == 'windows' elif lto_feature.enabled() or (lto_feature.auto() and get_option('buildtype').startswith('release')) cimba_lto = ['b_lto=true'] if get_option('default_library') != 'shared' - cimba_lto_args = ['-ffat-lto-objects'] + # Apple Clang does not accept -ffat-lto-objects + cimba_lto_args = cc.get_supported_arguments('-ffat-lto-objects') endif endif diff --git a/src/port/aarch64/macos/cmb_random_hwseed.c b/src/port/aarch64/macos/cmb_random_hwseed.c new file mode 100644 index 00000000..e2d4a9c3 --- /dev/null +++ b/src/port/aarch64/macos/cmb_random_hwseed.c @@ -0,0 +1,19 @@ +/* + * Random seed generation for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include + +#include "cmb_assert.h" +#include "cmb_random.h" + +uint64_t cmb_random_hwseed(void) +{ + uint64_t seed = 0u; + arc4random_buf(&seed, sizeof(seed)); + cmb_assert_debug(seed != 0u); + return seed; +} diff --git a/src/port/aarch64/macos/cmi_coroutine_context.S b/src/port/aarch64/macos/cmi_coroutine_context.S new file mode 100644 index 00000000..f7894333 --- /dev/null +++ b/src/port/aarch64/macos/cmi_coroutine_context.S @@ -0,0 +1,63 @@ +/* + * Context switch and coroutine trampoline for arm64 macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +.text +.p2align 2 + +.globl _cmi_coroutine_context_switch +_cmi_coroutine_context_switch: + sub sp, sp, #176 + stp x19, x20, [sp, #0] + stp x21, x22, [sp, #16] + stp x23, x24, [sp, #32] + stp x25, x26, [sp, #48] + stp x27, x28, [sp, #64] + stp x29, x30, [sp, #80] + stp d8, d9, [sp, #96] + stp d10, d11, [sp, #112] + stp d12, d13, [sp, #128] + stp d14, d15, [sp, #144] + mrs x9, fpcr + mrs x10, fpsr + stp x9, x10, [sp, #160] + + mov x9, sp + str x9, [x0] + ldr x9, [x1] + mov sp, x9 + + ldp x9, x10, [sp, #160] + msr fpcr, x9 + msr fpsr, x10 + ldp d14, d15, [sp, #144] + ldp d12, d13, [sp, #128] + ldp d10, d11, [sp, #112] + ldp d8, d9, [sp, #96] + ldp x29, x30, [sp, #80] + ldp x27, x28, [sp, #64] + ldp x25, x26, [sp, #48] + ldp x23, x24, [sp, #32] + ldp x21, x22, [sp, #16] + ldp x19, x20, [sp, #0] + add sp, sp, #176 + + mov x0, x2 + ret + +.p2align 2 +.globl _cmi_coroutine_trampoline +_cmi_coroutine_trampoline: + stp x29, x30, [sp, #-16]! + mov x29, sp + mov x0, x20 + mov x1, x21 + blr x19 + mov x16, x22 + ldp x29, x30, [sp], #16 + br x16 + +.subsections_via_symbols diff --git a/src/port/aarch64/macos/cmi_coroutine_context.c b/src/port/aarch64/macos/cmi_coroutine_context.c new file mode 100644 index 00000000..c95285ac --- /dev/null +++ b/src/port/aarch64/macos/cmi_coroutine_context.c @@ -0,0 +1,128 @@ +/* + * Coroutine stack initialization for arm64 macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include +#include +#include + +#include "cmb_assert.h" +#include "cmi_coroutine.h" +#include "cmi_memutils.h" +#include "cmi_sanitizer.h" + +#define CMI_ARM64_CONTEXT_SIZE 176u + +extern void cmi_coroutine_trampoline(void); +extern void *cmi_coroutine_launch(struct cmi_coroutine *cp, void *arg); + +bool cmi_coroutine_stack_valid(const struct cmi_coroutine *cp) +{ + cmb_assert_debug(cp != NULL); + cmb_assert_debug(cp->stack_base != NULL); + cmb_assert_debug(cp->stack_limit != NULL); + + const struct cmi_coroutine *cp_main = cmi_coroutine_main(); + if (cp == cp_main) { + cmb_assert_debug(cp->status == CMI_COROUTINE_RUNNING); + cmb_assert_debug(cp->stack == NULL); + } + else { + cmb_assert_debug(cp->stack != NULL); + cmb_assert_debug(cp->stack_pointer != NULL); + } + + if (cp->stack_pointer != NULL) { + cmb_assert_debug( + (uintptr_t)cp->stack_pointer > (uintptr_t)cp->stack_limit + ); + cmb_assert_debug( + (uintptr_t)cp->stack_pointer < (uintptr_t)cp->stack_base + ); + cmb_assert_debug(((uintptr_t)cp->stack_pointer % 16u) == 0u); + } + return true; +} + +void cmi_coroutine_context_init(struct cmi_coroutine *cp) +{ + cmb_assert_release(cp != NULL); + cmb_assert_debug(cp->stack != NULL); + + uintptr_t aligned_base = (uintptr_t)cp->stack_base & ~(uintptr_t)0x0fu; + unsigned char *stkptr = ( + (unsigned char *)aligned_base - CMI_ARM64_CONTEXT_SIZE + ); + (void)cmi_memset(stkptr, 0, CMI_ARM64_CONTEXT_SIZE); + + uintptr_t *registers = (uintptr_t *)stkptr; + registers[0] = (uintptr_t)cmi_coroutine_launch; /* X19 */ + registers[1] = (uintptr_t)cp; /* X20 */ + registers[2] = (uintptr_t)cp->context; /* X21 */ + registers[3] = (uintptr_t)( + cp->cr_exit == NULL ? cmi_coroutine_exit : cp->cr_exit + ); /* X22 */ + registers[11] = (uintptr_t)cmi_coroutine_trampoline; /* X30 */ + + cp->stack_pointer = stkptr; + cmb_assert_debug(cmi_coroutine_stack_valid(cp)); +} + +unsigned char *cmi_coroutine_stack_alloc(const size_t size, + unsigned char **base_p, + unsigned char **limit_p) +{ + const size_t pagesz = cmi_pagesize(); + unsigned char *raw = cmi_aligned_alloc(pagesz, size + pagesz); + cmb_assert_always(raw != NULL); + cmb_assert_always(mprotect(raw, pagesz, PROT_NONE) == 0); + + *base_p = raw + size + pagesz; + *limit_p = raw + pagesz; + return raw; +} + +void cmi_coroutine_stack_free(unsigned char *stack, size_t size) +{ + cmb_assert_release(stack != NULL); + const size_t pagesz = cmi_pagesize(); + cmi_asan_unpoison(stack + pagesz, size); + cmb_assert_always(mprotect(stack, pagesz, PROT_READ | PROT_WRITE) == 0); + cmi_aligned_free(stack); +} + +unsigned char *cmi_coroutine_stackbase(void) +{ + return pthread_get_stackaddr_np(pthread_self()); +} + +unsigned char *cmi_coroutine_stacklimit(void) +{ + unsigned char *base = pthread_get_stackaddr_np(pthread_self()); + return base - pthread_get_stacksize_np(pthread_self()); +} + +unsigned char *cmi_coroutine_stackraw(void) +{ + return NULL; +} + +/* macOS frees stacks immediately, so there is no recycled stack pool. */ +void cmi_coroutine_stack_cleanup(void) {} + +/* Darwin does not require updating OS stack bounds after longjmp. */ +void cmi_coroutine_os_adopt_stack(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); +} + +/* Both Darwin ports preserve floating-point control state on every switch. */ +bool cmi_coroutine_registers_valid(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); + return true; +} diff --git a/src/port/aarch64/macos/cmi_cpu_cores.c b/src/port/aarch64/macos/cmi_cpu_cores.c new file mode 100644 index 00000000..02054ab5 --- /dev/null +++ b/src/port/aarch64/macos/cmi_cpu_cores.c @@ -0,0 +1,15 @@ +/* + * CPU availability for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include + +uint32_t cmi_cpu_cores(void) +{ + const long count = sysconf(_SC_NPROCESSORS_ONLN); + return count > 0 ? (uint32_t)count : 1u; +} diff --git a/src/port/aarch64/macos/cmi_memutils.c b/src/port/aarch64/macos/cmi_memutils.c new file mode 100644 index 00000000..c36b081e --- /dev/null +++ b/src/port/aarch64/macos/cmi_memutils.c @@ -0,0 +1,46 @@ +/* + * Aligned allocation helpers for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include +#include +#include + +#include "cmb_assert.h" +#include "cmi_memutils.h" + +size_t cmi_pagesize(void) +{ + return (size_t)sysconf(_SC_PAGESIZE); +} + +void *cmi_aligned_alloc(const size_t align, const size_t sz) +{ + void *ptr = NULL; + const int result = posix_memalign(&ptr, align, sz); + cmb_assert_always(result == 0); + cmb_assert_always(ptr != NULL); + return ptr; +} + +void cmi_aligned_free(void *ptr) +{ + cmb_assert_release(ptr != NULL); + free(ptr); +} + +void *cmi_aligned_realloc(void *ptr, + const size_t align, + const size_t sz) +{ + cmb_assert_release(ptr != NULL); + void *replacement = cmi_aligned_alloc(align, sz); + const size_t old_size = malloc_size(ptr); + memcpy(replacement, ptr, old_size < sz ? old_size : sz); + free(ptr); + return replacement; +} diff --git a/src/port/aarch64/macos/meson.build b/src/port/aarch64/macos/meson.build new file mode 100644 index 00000000..e73b30f0 --- /dev/null +++ b/src/port/aarch64/macos/meson.build @@ -0,0 +1,7 @@ +# arm64/macOS/meson.build +sources += files('cmb_random_hwseed.c', + 'cmi_coroutine_context.c', + 'cmi_coroutine_context.S', + 'cmi_cpu_cores.c', + 'cmi_memutils.c' +) diff --git a/src/port/meson.build b/src/port/meson.build index 654071ab..1b650f6d 100644 --- a/src/port/meson.build +++ b/src/port/meson.build @@ -1,2 +1,9 @@ -# just passing through -subdir('x86-64') +# port/meson.build + +if host_machine.cpu_family() == 'x86_64' + subdir('x86-64') +elif host_machine.cpu_family() == 'aarch64' and host_machine.system() == 'darwin' + subdir('aarch64/macos') +else + error('Unsupported platform: ' + host_machine.system() + '/' + host_machine.cpu_family()) +endif diff --git a/src/port/x86-64/macos/cmb_random_hwseed.c b/src/port/x86-64/macos/cmb_random_hwseed.c new file mode 100644 index 00000000..e2d4a9c3 --- /dev/null +++ b/src/port/x86-64/macos/cmb_random_hwseed.c @@ -0,0 +1,19 @@ +/* + * Random seed generation for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include + +#include "cmb_assert.h" +#include "cmb_random.h" + +uint64_t cmb_random_hwseed(void) +{ + uint64_t seed = 0u; + arc4random_buf(&seed, sizeof(seed)); + cmb_assert_debug(seed != 0u); + return seed; +} diff --git a/src/port/x86-64/macos/cmi_coroutine_context.asm b/src/port/x86-64/macos/cmi_coroutine_context.asm new file mode 100644 index 00000000..18db6e70 --- /dev/null +++ b/src/port/x86-64/macos/cmi_coroutine_context.asm @@ -0,0 +1,59 @@ +; +; Context switch and coroutine trampoline for x86-64 macOS. +; Mach-O symbols use a leading underscore; the register convention is SysV. +; +; Copyright (c) FBarrca 2026. +; Licensed under the Apache License, Version 2.0. + +bits 64 +default rel + +section .text +global _cmi_coroutine_context_switch +global _cmi_coroutine_trampoline + +%macro save_context 0 + pushfq + %ifndef NMXCSR + sub rsp, 8 + stmxcsr [rsp + 4] + %endif + push rbp + push rbx + push r12 + push r13 + push r14 + push r15 +%endmacro + +%macro load_context 0 + pop r15 + pop r14 + pop r13 + pop r12 + pop rbx + pop rbp + %ifndef NMXCSR + ldmxcsr [rsp + 4] + add rsp, 8 + %endif + popfq +%endmacro + +_cmi_coroutine_context_switch: + save_context + mov [rdi], rsp + mov rsp, [rsi] + load_context + mov rax, rdx + pop r9 + jmp r9 + +_cmi_coroutine_trampoline: + mov rdi, r13 + mov rsi, r14 + xor rax, rax + call r12 + push rdi + mov rdi, rax + jmp r15 diff --git a/src/port/x86-64/macos/cmi_coroutine_context.c b/src/port/x86-64/macos/cmi_coroutine_context.c new file mode 100644 index 00000000..d70553b3 --- /dev/null +++ b/src/port/x86-64/macos/cmi_coroutine_context.c @@ -0,0 +1,146 @@ +/* + * Coroutine stack initialization for x86-64 macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include +#include +#include + +#include "cmb_assert.h" +#include "cmi_coroutine.h" +#include "cmi_memutils.h" +#include "cmi_sanitizer.h" + +extern void cmi_coroutine_trampoline(void); +extern void *cmi_coroutine_launch(struct cmi_coroutine *cp, void *arg); + +bool cmi_coroutine_stack_valid(const struct cmi_coroutine *cp) +{ + cmb_assert_debug(cp != NULL); + cmb_assert_debug(cp->stack_base != NULL); + cmb_assert_debug(cp->stack_limit != NULL); + + const struct cmi_coroutine *cp_main = cmi_coroutine_main(); + if (cp == cp_main) { + cmb_assert_debug(cp->status == CMI_COROUTINE_RUNNING); + cmb_assert_debug(cp->stack == NULL); + } + else { + cmb_assert_debug(cp->stack != NULL); + cmb_assert_debug(cp->stack_pointer != NULL); + } + + if (cp->stack_pointer != NULL) { + cmb_assert_debug( + (uintptr_t)cp->stack_pointer > (uintptr_t)cp->stack_limit + ); + cmb_assert_debug( + (uintptr_t)cp->stack_pointer < (uintptr_t)cp->stack_base + ); + #ifndef NMXCSR + cmb_assert_debug( + (((uintptr_t)cp->stack_pointer + 8u) % 16u) == 0u + ); + #else + cmb_assert_debug(((uintptr_t)cp->stack_pointer % 16u) == 0u); + #endif + } + return true; +} + +void cmi_coroutine_context_init(struct cmi_coroutine *cp) +{ + cmb_assert_release(cp != NULL); + cmb_assert_debug(cp->stack != NULL); + + uintptr_t aligned_base = (uintptr_t)cp->stack_base & ~(uintptr_t)0x0fu; + unsigned char *stkptr = (unsigned char *)aligned_base; + + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)cmi_coroutine_trampoline; + stkptr -= 8u; + *(uint64_t *)stkptr = 0x0202ull; + + #ifndef NMXCSR + stkptr -= 8u; + *(uint32_t *)(stkptr + 4u) = 0x1f80u; + *(uint32_t *)stkptr = 0u; + #endif + + stkptr -= 8u; + *(uint64_t *)stkptr = 0u; /* RBP */ + stkptr -= 8u; + *(uint64_t *)stkptr = 0u; /* RBX */ + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)cmi_coroutine_launch; /* R12 */ + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)cp; /* R13 */ + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)cp->context; /* R14 */ + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)( + cp->cr_exit == NULL ? cmi_coroutine_exit : cp->cr_exit + ); /* R15 */ + + cp->stack_pointer = stkptr; + cmb_assert_debug(cmi_coroutine_stack_valid(cp)); +} + +unsigned char *cmi_coroutine_stack_alloc(const size_t size, + unsigned char **base_p, + unsigned char **limit_p) +{ + const size_t pagesz = cmi_pagesize(); + unsigned char *raw = cmi_aligned_alloc(pagesz, size + pagesz); + cmb_assert_always(raw != NULL); + cmb_assert_always(mprotect(raw, pagesz, PROT_NONE) == 0); + + *base_p = raw + size + pagesz; + *limit_p = raw + pagesz; + return raw; +} + +void cmi_coroutine_stack_free(unsigned char *stack, size_t size) +{ + cmb_assert_release(stack != NULL); + const size_t pagesz = cmi_pagesize(); + cmi_asan_unpoison(stack + pagesz, size); + cmb_assert_always(mprotect(stack, pagesz, PROT_READ | PROT_WRITE) == 0); + cmi_aligned_free(stack); +} + +unsigned char *cmi_coroutine_stackbase(void) +{ + return pthread_get_stackaddr_np(pthread_self()); +} + +unsigned char *cmi_coroutine_stacklimit(void) +{ + unsigned char *base = pthread_get_stackaddr_np(pthread_self()); + return base - pthread_get_stacksize_np(pthread_self()); +} + +unsigned char *cmi_coroutine_stackraw(void) +{ + return NULL; +} + +/* macOS frees stacks immediately, so there is no recycled stack pool. */ +void cmi_coroutine_stack_cleanup(void) {} + +/* Darwin does not require updating OS stack bounds after longjmp. */ +void cmi_coroutine_os_adopt_stack(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); +} + +/* Both Darwin ports preserve floating-point control state on every switch. */ +bool cmi_coroutine_registers_valid(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); + return true; +} diff --git a/src/port/x86-64/macos/cmi_cpu_cores.c b/src/port/x86-64/macos/cmi_cpu_cores.c new file mode 100644 index 00000000..02054ab5 --- /dev/null +++ b/src/port/x86-64/macos/cmi_cpu_cores.c @@ -0,0 +1,15 @@ +/* + * CPU availability for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include + +uint32_t cmi_cpu_cores(void) +{ + const long count = sysconf(_SC_NPROCESSORS_ONLN); + return count > 0 ? (uint32_t)count : 1u; +} diff --git a/src/port/x86-64/macos/cmi_memutils.c b/src/port/x86-64/macos/cmi_memutils.c new file mode 100644 index 00000000..c36b081e --- /dev/null +++ b/src/port/x86-64/macos/cmi_memutils.c @@ -0,0 +1,46 @@ +/* + * Aligned allocation helpers for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include +#include +#include + +#include "cmb_assert.h" +#include "cmi_memutils.h" + +size_t cmi_pagesize(void) +{ + return (size_t)sysconf(_SC_PAGESIZE); +} + +void *cmi_aligned_alloc(const size_t align, const size_t sz) +{ + void *ptr = NULL; + const int result = posix_memalign(&ptr, align, sz); + cmb_assert_always(result == 0); + cmb_assert_always(ptr != NULL); + return ptr; +} + +void cmi_aligned_free(void *ptr) +{ + cmb_assert_release(ptr != NULL); + free(ptr); +} + +void *cmi_aligned_realloc(void *ptr, + const size_t align, + const size_t sz) +{ + cmb_assert_release(ptr != NULL); + void *replacement = cmi_aligned_alloc(align, sz); + const size_t old_size = malloc_size(ptr); + memcpy(replacement, ptr, old_size < sz ? old_size : sz); + free(ptr); + return replacement; +} diff --git a/src/port/x86-64/macos/meson.build b/src/port/x86-64/macos/meson.build new file mode 100644 index 00000000..db0a96e6 --- /dev/null +++ b/src/port/x86-64/macos/meson.build @@ -0,0 +1,13 @@ +# x86-64/macOS/meson.build +nasm_args += ['-f', 'macho64'] + +sources += files('cmb_random_hwseed.c', + 'cmi_coroutine_context.c', + 'cmi_cpu_cores.c', + 'cmi_memutils.c' +) + +sources += generator(find_program('nasm'), + output : '@BASENAME@.o', + arguments : nasm_args + ['@INPUT@', '-o', '@OUTPUT@'] +).process('cmi_coroutine_context.asm') diff --git a/src/port/x86-64/meson.build b/src/port/x86-64/meson.build index fc449e3d..70633424 100644 --- a/src/port/x86-64/meson.build +++ b/src/port/x86-64/meson.build @@ -4,6 +4,8 @@ if host_machine.system() == 'linux' subdir('linux') elif host_machine.system() == 'windows' subdir('windows') +elif host_machine.system() == 'darwin' + subdir('macos') else error('Unsupported platform: ' + host_machine.system()) endif From bd89e7fb598fc01f901eaa029c946b9974bb381f Mon Sep 17 00:00:00 2001 From: FBarrca Date: Tue, 3 Nov 2026 14:00:01 +0100 Subject: [PATCH 2/2] Add arm64 Linux port Context switch shares the arm64 macOS frame layout (X19-X30, D8-D15, FPCR/FPSR) in ELF form. Stack allocation keeps the Linux per-thread pool of recycled guarded stacks and pthread_getattr_np main stack bounds. Seeds come from getrandom(), page size, aligned allocation and core count reuse the x86-64 Linux sources. initial-exec TLS sets DF_STATIC_TLS, and glibc on aarch64 refuses to dlopen such a library late in the process ("cannot allocate memory in static TLS block"), which breaks loading it from Python. Keep initial-exec on x86-64 Linux only; arm64 uses TLS descriptors. Distribution GCCs enable _FORTIFY_SOURCE, which turns longjmp into __longjmp_chk. That check aborts ("longjmp causes uninitialized stack frame") when an abandoned trial jumps from a coroutine stack back to the worker stack, so it is disabled for cimba.c on arm64 Linux. Co-Authored-By: Claude Includes-AI-Code: true --- meson.build | 3 +- src/cimba.c | 8 + src/cmi_config.h | 13 +- src/port/aarch64/linux/cmb_random_hwseed.c | 38 +++ .../aarch64/linux/cmi_coroutine_context.S | 76 ++++++ .../aarch64/linux/cmi_coroutine_context.c | 240 ++++++++++++++++++ src/port/aarch64/linux/meson.build | 9 + src/port/meson.build | 2 + 8 files changed, 383 insertions(+), 6 deletions(-) create mode 100644 src/port/aarch64/linux/cmb_random_hwseed.c create mode 100644 src/port/aarch64/linux/cmi_coroutine_context.S create mode 100644 src/port/aarch64/linux/cmi_coroutine_context.c create mode 100644 src/port/aarch64/linux/meson.build diff --git a/meson.build b/meson.build index ad8db781..b9381307 100644 --- a/meson.build +++ b/meson.build @@ -90,7 +90,8 @@ elif get_option('pgo') == 'use' desired_flags += pgo_flags endif -if host_machine.system() == 'linux' +# Matches CMB_THREAD_LOCAL in cmi_config.h: initial-exec on x86-64 Linux only +if host_machine.system() == 'linux' and host_machine.cpu_family() == 'x86_64' desired_flags += ['-ftls-model=initial-exec' ] endif diff --git a/src/cimba.c b/src/cimba.c index 6b860461..35c5806c 100644 --- a/src/cimba.c +++ b/src/cimba.c @@ -21,6 +21,14 @@ * limitations under the License. */ +/* glibc's _FORTIFY_SOURCE (on by default in e.g. Ubuntu's GCC) replaces + * longjmp with __longjmp_chk, which aborts with "longjmp causes uninitialized + * stack frame" when a trial abandons from a coroutine stack back to the worker + * stack. x86-64 uses __builtin_longjmp; arm64 Linux needs the plain longjmp. */ +#if defined(__linux__) && defined(__aarch64__) +#undef _FORTIFY_SOURCE +#endif + #include #include #include diff --git a/src/cmi_config.h b/src/cmi_config.h index 25c7933e..8ab87d28 100644 --- a/src/cmi_config.h +++ b/src/cmi_config.h @@ -23,7 +23,7 @@ /* * Supported toolchains: - * - Architecture: x86-64 / AMD64, or arm64 (Apple Silicon) + * - Architecture: x86-64 / AMD64, or arm64 (Apple Silicon, Linux) * - Operating system: Linux, Windows via MinGW-w64, or macOS * - Compiler: GCC or Clang, C11 or later * (the library itself is built as C23) @@ -33,7 +33,7 @@ /* * Identify the processor architecture. AMD64/x86-64 everywhere, and arm64 on - * macOS (Apple Silicon). + * macOS (Apple Silicon) and Linux. */ #define CMI_AMD64 1 #define CMI_ARM64 2 @@ -128,10 +128,13 @@ __attribute__((format(printf, fmt_idx, first_arg))) /* - * Thread-local storage qualifier. _Thread_local is C11; on Linux we pin the TLS - * model to initial-exec for fast access from the worker threads. + * Thread-local storage qualifier. _Thread_local is C11; on x86-64 Linux we pin + * the TLS model to initial-exec for fast access from the worker threads. Not on + * arm64 Linux: a library dlopen'ed late (e.g. from Python) can then fail with + * "cannot allocate memory in static TLS block", and the default TLS descriptor + * model is already cheap there. */ -#if CMI_OS == CMI_LINUX +#if CMI_OS == CMI_LINUX && CMI_ARCH == CMI_AMD64 #define CMB_THREAD_LOCAL _Thread_local __attribute__((tls_model("initial-exec"))) #else #define CMB_THREAD_LOCAL _Thread_local diff --git a/src/port/aarch64/linux/cmb_random_hwseed.c b/src/port/aarch64/linux/cmb_random_hwseed.c new file mode 100644 index 00000000..ace1f9ff --- /dev/null +++ b/src/port/aarch64/linux/cmb_random_hwseed.c @@ -0,0 +1,38 @@ +/* + * Random seed generation for arm64 Linux. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include +#include +#include + +#include "cmb_assert.h" +#include "cmb_random.h" + +uint64_t cmb_random_hwseed(void) +{ + uint64_t seed = 0u; + if (getrandom(&seed, sizeof(seed), 0) != (ssize_t)sizeof(seed)) { + seed = 0u; + } + + if (seed == 0u) { + /* No kernel entropy, mash the thread id, clock and virtual counter */ + seed = (uint64_t)pthread_self() << 32; + struct timespec ts; + const int r = clock_gettime(CLOCK_REALTIME, &ts); + cmb_assert_release(r == 0); + seed += (uint64_t)(ts.tv_nsec ^ ts.tv_sec); + + uint64_t ticks; + __asm__ volatile("mrs %0, cntvct_el0" : "=r"(ticks)); + seed ^= ticks; + } + + cmb_assert_debug(seed != 0u); + return seed; +} diff --git a/src/port/aarch64/linux/cmi_coroutine_context.S b/src/port/aarch64/linux/cmi_coroutine_context.S new file mode 100644 index 00000000..362b90a2 --- /dev/null +++ b/src/port/aarch64/linux/cmi_coroutine_context.S @@ -0,0 +1,76 @@ +/* + * Context switch and coroutine trampoline for arm64 Linux (AAPCS64, ELF). + * + * Same frame layout as the arm64 macOS port: callee-saved X19-X30, D8-D15, + * then FPCR and FPSR, 176 bytes in total. See cmi_coroutine_context.c for the + * initial frame of a new coroutine. + * + * The object carries no GNU_PROPERTY_AARCH64_FEATURE_1_AND note, so the + * linker clears BTI/PAC marking from any binary that links Cimba. The + * hand-rolled stack switch returns into frames it did not call, which BTI + * landing pads and return address signing are not designed for. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + + .text + .p2align 2 + + .globl cmi_coroutine_context_switch + .type cmi_coroutine_context_switch, %function +cmi_coroutine_context_switch: + sub sp, sp, #176 + stp x19, x20, [sp, #0] + stp x21, x22, [sp, #16] + stp x23, x24, [sp, #32] + stp x25, x26, [sp, #48] + stp x27, x28, [sp, #64] + stp x29, x30, [sp, #80] + stp d8, d9, [sp, #96] + stp d10, d11, [sp, #112] + stp d12, d13, [sp, #128] + stp d14, d15, [sp, #144] + mrs x9, fpcr + mrs x10, fpsr + stp x9, x10, [sp, #160] + + mov x9, sp + str x9, [x0] + ldr x9, [x1] + mov sp, x9 + + ldp x9, x10, [sp, #160] + msr fpcr, x9 + msr fpsr, x10 + ldp d14, d15, [sp, #144] + ldp d12, d13, [sp, #128] + ldp d10, d11, [sp, #112] + ldp d8, d9, [sp, #96] + ldp x29, x30, [sp, #80] + ldp x27, x28, [sp, #64] + ldp x25, x26, [sp, #48] + ldp x23, x24, [sp, #32] + ldp x21, x22, [sp, #16] + ldp x19, x20, [sp, #0] + add sp, sp, #176 + + mov x0, x2 + ret + .size cmi_coroutine_context_switch, . - cmi_coroutine_context_switch + + .p2align 2 + .globl cmi_coroutine_trampoline + .type cmi_coroutine_trampoline, %function +cmi_coroutine_trampoline: + stp x29, x30, [sp, #-16]! + mov x29, sp + mov x0, x20 + mov x1, x21 + blr x19 + mov x16, x22 + ldp x29, x30, [sp], #16 + br x16 + .size cmi_coroutine_trampoline, . - cmi_coroutine_trampoline + + .section .note.GNU-stack, "", %progbits diff --git a/src/port/aarch64/linux/cmi_coroutine_context.c b/src/port/aarch64/linux/cmi_coroutine_context.c new file mode 100644 index 00000000..77ff8f96 --- /dev/null +++ b/src/port/aarch64/linux/cmi_coroutine_context.c @@ -0,0 +1,240 @@ +/* + * Coroutine stack initialization for arm64 Linux. + * + * The initial frame matches cmi_coroutine_context.S (shared with the arm64 + * macOS port), while stack allocation and main stack discovery follow the + * x86-64 Linux port: a per-thread pool of recycled guarded stacks, and + * pthread_getattr_np for the main stack bounds. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +/* Make sure we get pthread_getattr_np */ +#define _GNU_SOURCE // NOLINT(bugprone-reserved-identifier) +#include + +#include +#include +#include + +#include "cmb_assert.h" +#include "cmi_coroutine.h" +#include "cmi_memutils.h" +#include "cmi_sanitizer.h" + +#define CMI_ARM64_CONTEXT_SIZE 176u + +extern void cmi_coroutine_trampoline(void); +extern void *cmi_coroutine_launch(struct cmi_coroutine *cp, void *arg); + +/* + * Intrusive singly linked list of recycled stacks, as in the x86-64 Linux + * port: mprotect is badly serializing for multithreaded applications, so + * stacks are recycled within the current thread and freed at thread exit. + */ +struct stack_tag { + size_t size; /* Size of stacks in this list */ + unsigned char *head; /* The intrusive list */ + struct stack_tag *next; /* Next list, a different size stack */ +}; + +CMB_THREAD_LOCAL static struct stack_tag *stack_list = NULL; + +bool cmi_coroutine_stack_valid(const struct cmi_coroutine *cp) +{ + cmb_assert_debug(cp != NULL); + cmb_assert_debug(cp->stack_base != NULL); + cmb_assert_debug(cp->stack_limit != NULL); + + const struct cmi_coroutine *cp_main = cmi_coroutine_main(); + if (cp == cp_main) { + cmb_assert_debug(cp->status == CMI_COROUTINE_RUNNING); + cmb_assert_debug(cp->stack == NULL); + } + else { + cmb_assert_debug(cp->stack != NULL); + cmb_assert_debug(cp->stack_pointer != NULL); + } + + if (cp->stack_pointer != NULL) { + cmb_assert_debug( + (uintptr_t)cp->stack_pointer > (uintptr_t)cp->stack_limit + ); + cmb_assert_debug( + (uintptr_t)cp->stack_pointer < (uintptr_t)cp->stack_base + ); + cmb_assert_debug(((uintptr_t)cp->stack_pointer % 16u) == 0u); + } + return true; +} + +void cmi_coroutine_context_init(struct cmi_coroutine *cp) +{ + cmb_assert_release(cp != NULL); + cmb_assert_debug(cp->stack != NULL); + + uintptr_t aligned_base = (uintptr_t)cp->stack_base & ~(uintptr_t)0x0fu; + unsigned char *stkptr = ( + (unsigned char *)aligned_base - CMI_ARM64_CONTEXT_SIZE + ); + (void)cmi_memset(stkptr, 0, CMI_ARM64_CONTEXT_SIZE); + + uintptr_t *registers = (uintptr_t *)stkptr; + registers[0] = (uintptr_t)cmi_coroutine_launch; /* X19 */ + registers[1] = (uintptr_t)cp; /* X20 */ + registers[2] = (uintptr_t)cp->context; /* X21 */ + registers[3] = (uintptr_t)( + cp->cr_exit == NULL ? cmi_coroutine_exit : cp->cr_exit + ); /* X22 */ + registers[11] = (uintptr_t)cmi_coroutine_trampoline; /* X30 */ + + /* Inherit the parent's FPCR (rounding mode etc.), start with clear FPSR */ + uint64_t fpcr; + __asm__ volatile("mrs %0, fpcr" : "=r"(fpcr)); + registers[20] = (uintptr_t)fpcr; + + cp->stack_pointer = stkptr; + cmb_assert_debug(cmi_coroutine_stack_valid(cp)); +} + +unsigned char *cmi_coroutine_stack_alloc(const size_t size_req, + unsigned char **base_p, + unsigned char **limit_p) +{ + cmb_assert_debug(size_req > 0u); + cmb_assert_debug((base_p != NULL) && (limit_p != NULL)); + + const size_t pagesz = cmi_pagesize(); + cmb_assert_debug(size_req <= SIZE_MAX - pagesz); + const size_t size_rnd = (size_req + pagesz - 1u) & ~(pagesz - 1u); + + /* Do we have one lying around? */ + unsigned char *stack_raw = NULL; + struct stack_tag *st = stack_list; + while (st != NULL) { + if (st->size == size_rnd) { + if (st->head != NULL) { + stack_raw = st->head; + cmi_asan_unpoison(stack_raw + pagesz, size_rnd); + unsigned char **nextloc = (unsigned char **)(stack_raw + pagesz); + st->head = *nextloc; + } + break; + } + st = st->next; + } + + if (stack_raw == NULL) { + stack_raw = cmi_aligned_alloc(pagesz, size_rnd + pagesz); + cmb_assert_always(stack_raw != NULL); + const int r = mprotect(stack_raw, pagesz, PROT_NONE); + cmb_assert_always(r == 0); + } + + *base_p = stack_raw + pagesz + size_rnd; + *limit_p = stack_raw + pagesz; + cmb_assert_debug(!cmi_asan_region_poisoned(stack_raw + pagesz, size_rnd)); + + return stack_raw; +} + +/* Push a stack back on the pool rather than freeing it */ +void cmi_coroutine_stack_free(unsigned char *stack_raw, size_t size_req) +{ + cmb_assert_release(stack_raw != NULL); + + const size_t pagesz = cmi_pagesize(); + cmb_assert_debug(size_req <= SIZE_MAX - pagesz); + const size_t size_rnd = (size_req + pagesz - 1u) & ~(pagesz - 1u); + cmi_asan_unpoison(stack_raw + pagesz, size_rnd); + + struct stack_tag *st = stack_list; + while (st != NULL) { + if (st->size == size_rnd) { + unsigned char **nextloc = (unsigned char **)(stack_raw + pagesz); + *nextloc = st->head; + st->head = stack_raw; + return; + } + st = st->next; + } + + st = cmi_malloc(sizeof(*st)); + *(unsigned char **)(stack_raw + pagesz) = NULL; + st->size = size_rnd; + st->head = stack_raw; + st->next = stack_list; + stack_list = st; +} + +void cmi_coroutine_stack_cleanup(void) +{ + const size_t pagesz = cmi_pagesize(); + struct stack_tag *st = stack_list; + while (st != NULL) { + while (st->head != NULL) { + unsigned char *raw = st->head; + unsigned char *next = *(unsigned char **)(raw + pagesz); + const int r = mprotect(raw, pagesz, PROT_READ | PROT_WRITE); + cmb_assert_always(r == 0); + cmi_aligned_free(raw); + st->head = next; + } + + struct stack_tag *next = st->next; + cmi_free(st); + st = next; + } + + stack_list = NULL; +} + +static void main_stack_bounds(unsigned char **low_p, size_t *size_p) +{ + pthread_attr_t attrs; + pthread_attr_init(&attrs); + int r = pthread_getattr_np(pthread_self(), &attrs); + cmb_assert_release(r == 0); + + void *low; + r = pthread_attr_getstack(&attrs, &low, size_p); + cmb_assert_release(r == 0); + pthread_attr_destroy(&attrs); + + *low_p = low; +} + +unsigned char *cmi_coroutine_stackbase(void) +{ + unsigned char *low; + size_t size; + main_stack_bounds(&low, &size); + return low + size; +} + +unsigned char *cmi_coroutine_stacklimit(void) +{ + unsigned char *low; + size_t size; + main_stack_bounds(&low, &size); + return low; +} + +unsigned char *cmi_coroutine_stackraw(void) +{ + return NULL; +} + +/* Linux does not require updating OS stack bounds after longjmp. */ +void cmi_coroutine_os_adopt_stack(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); +} + +/* The context switch preserves FPCR and FPSR on every switch. */ +bool cmi_coroutine_registers_valid(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); + return true; +} diff --git a/src/port/aarch64/linux/meson.build b/src/port/aarch64/linux/meson.build new file mode 100644 index 00000000..91258eb9 --- /dev/null +++ b/src/port/aarch64/linux/meson.build @@ -0,0 +1,9 @@ +# aarch64/Linux/meson.build +# Page size, aligned allocation and core count are architecture independent, +# so reuse the x86-64 Linux versions rather than keeping a second copy. +sources += files('cmb_random_hwseed.c', + 'cmi_coroutine_context.c', + 'cmi_coroutine_context.S', + '../../x86-64/linux/cmi_cpu_cores.c', + '../../x86-64/linux/cmi_memutils.c' +) diff --git a/src/port/meson.build b/src/port/meson.build index 1b650f6d..fabb899c 100644 --- a/src/port/meson.build +++ b/src/port/meson.build @@ -4,6 +4,8 @@ if host_machine.cpu_family() == 'x86_64' subdir('x86-64') elif host_machine.cpu_family() == 'aarch64' and host_machine.system() == 'darwin' subdir('aarch64/macos') +elif host_machine.cpu_family() == 'aarch64' and host_machine.system() == 'linux' + subdir('aarch64/linux') else error('Unsupported platform: ' + host_machine.system() + '/' + host_machine.cpu_family()) endif