diff --git a/CMakeLists.txt b/CMakeLists.txt index 11bd950..da71278 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -391,6 +391,7 @@ endif() set(3PROXY_CORE_SOURCES src/3proxy.c src/loop.c + src/fiber.c src/auth.c src/acl.c src/limiter.c @@ -628,13 +629,17 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_libraries(3proxy PRIVATE dl) endif() -# The event loop on its own, for tests/cases/loop.py: not installed +# The event loop and fibers on their own, for tests/cases/units.py: not +# installed add_executable(looptest tests/looptest.c src/loop.c) -target_include_directories(looptest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_link_libraries(looptest PRIVATE Threads::Threads) -if(WIN32) - target_link_libraries(looptest PRIVATE ${WINDOWS_LIBS}) -endif() +add_executable(fibertest tests/fibertest.c src/fiber.c src/loop.c) +foreach(unit looptest fibertest) + target_include_directories(${unit} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(${unit} PRIVATE Threads::Threads) + if(WIN32) + target_link_libraries(${unit} PRIVATE ${WINDOWS_LIBS}) + endif() +endforeach() # Build 3proxy_crypt utility add_executable(3proxy_crypt diff --git a/src/Makefile.inc b/src/Makefile.inc index 7f6d8d4..829ce25 100644 --- a/src/Makefile.inc +++ b/src/Makefile.inc @@ -34,6 +34,9 @@ sockgetchar$(OBJSUFFICS): sockgetchar.c proxy.h structures.h loop$(OBJSUFFICS): loop.c loop.h proxy.h structures.h $(CC) $(CFLAGS) loop.c +fiber$(OBJSUFFICS): fiber.c fiber.h proxy.h structures.h + $(CC) $(CFLAGS) fiber.c + proxy$(OBJSUFFICS): proxy.c proxy.h structures.h $(PROXYMAIN) $(CC) $(CFLAGS) $(DEFINEOPTION)WITHMAIN $(DEFINEOPTION)NOPORTMAP $(DEFINEOPTION)ANONYMOUS $(DEFINEOPTION)NOUDPMAIN proxy.c @@ -203,6 +206,6 @@ ssl$(OBJSUFFICS): ssl.c structures.h proxy.h ssl.h pcre$(OBJSUFFICS): pcre.c structures.h $(CC) $(COUT)pcre$(OBJSUFFICS) $(CFLAGS) $(DEFINEOPTION)WITH_PCRE pcre.c -$(BUILDDIR)3proxy$(EXESUFFICS): 3proxy$(OBJSUFFICS) loop$(OBJSUFFICS) mainfunc$(OBJSUFFICS) srvproxy$(OBJSUFFICS) srvpop3p$(OBJSUFFICS) srvimapp$(OBJSUFFICS) srvsmtpp$(OBJSUFFICS) srvftppr$(OBJSUFFICS) srvsocks$(OBJSUFFICS) srvtcppm$(OBJSUFFICS) srvtlspr$(OBJSUFFICS) srvauto$(OBJSUFFICS) srvudppm$(OBJSUFFICS) sockmap$(OBJSUFFICS) udpsockmap$(OBJSUFFICS) sockgetchar$(OBJSUFFICS) common$(OBJSUFFICS) sem$(OBJSUFFICS) auth$(OBJSUFFICS) acl$(OBJSUFFICS) limiter$(OBJSUFFICS) redirect$(OBJSUFFICS) authradius$(OBJSUFFICS) hash$(OBJSUFFICS) hashtables$(OBJSUFFICS) resolve$(OBJSUFFICS) sql$(OBJSUFFICS) conf$(OBJSUFFICS) log$(OBJSUFFICS) datatypes$(OBJSUFFICS) md4$(OBJSUFFICS) md5$(OBJSUFFICS) blake2$(OBJSUFFICS) 3proxy_crypt$(OBJSUFFICS) base64$(OBJSUFFICS) ftp$(OBJSUFFICS) stringtable$(OBJSUFFICS) $(HTTPSRV_OBJS) srvdnspr$(OBJSUFFICS) plugins$(OBJSUFFICS) mdhash$(OBJSUFFICS) $(SSL_OBJS) $(PCRE_OBJS) $(TRANSPARENT_OBJS) $(COMPATLIBS) $(VERSIONDEP) - $(LN) $(LNOUT)$(BUILDDIR)3proxy$(EXESUFFICS) $(LDFLAGS) $(VERFILE) 3proxy$(OBJSUFFICS) loop$(OBJSUFFICS) mainfunc$(OBJSUFFICS) auth$(OBJSUFFICS) acl$(OBJSUFFICS) limiter$(OBJSUFFICS) redirect$(OBJSUFFICS) authradius$(OBJSUFFICS) hash$(OBJSUFFICS) hashtables$(OBJSUFFICS) resolve$(OBJSUFFICS) sql$(OBJSUFFICS) conf$(OBJSUFFICS) datatypes$(OBJSUFFICS) srvauto$(OBJSUFFICS) srvproxy$(OBJSUFFICS) srvpop3p$(OBJSUFFICS) srvimapp$(OBJSUFFICS) srvsmtpp$(OBJSUFFICS) srvftppr$(OBJSUFFICS) srvsocks$(OBJSUFFICS) srvtcppm$(OBJSUFFICS) srvtlspr$(OBJSUFFICS) srvudppm$(OBJSUFFICS) sockmap$(OBJSUFFICS) udpsockmap$(OBJSUFFICS) sockgetchar$(OBJSUFFICS) common$(OBJSUFFICS) sem$(OBJSUFFICS) log$(OBJSUFFICS) 3proxy_crypt$(OBJSUFFICS) md4$(OBJSUFFICS) md5$(OBJSUFFICS) blake2$(OBJSUFFICS) base64$(OBJSUFFICS) ftp$(OBJSUFFICS) stringtable$(OBJSUFFICS) $(HTTPSRV_OBJS) srvdnspr$(OBJSUFFICS) plugins$(OBJSUFFICS) mdhash$(OBJSUFFICS) $(SSL_OBJS) $(PCRE_OBJS) $(TRANSPARENT_OBJS) $(COMPATLIBS) $(LIBS) $(PCRE_LIBS) +$(BUILDDIR)3proxy$(EXESUFFICS): 3proxy$(OBJSUFFICS) loop$(OBJSUFFICS) fiber$(OBJSUFFICS) mainfunc$(OBJSUFFICS) srvproxy$(OBJSUFFICS) srvpop3p$(OBJSUFFICS) srvimapp$(OBJSUFFICS) srvsmtpp$(OBJSUFFICS) srvftppr$(OBJSUFFICS) srvsocks$(OBJSUFFICS) srvtcppm$(OBJSUFFICS) srvtlspr$(OBJSUFFICS) srvauto$(OBJSUFFICS) srvudppm$(OBJSUFFICS) sockmap$(OBJSUFFICS) udpsockmap$(OBJSUFFICS) sockgetchar$(OBJSUFFICS) common$(OBJSUFFICS) sem$(OBJSUFFICS) auth$(OBJSUFFICS) acl$(OBJSUFFICS) limiter$(OBJSUFFICS) redirect$(OBJSUFFICS) authradius$(OBJSUFFICS) hash$(OBJSUFFICS) hashtables$(OBJSUFFICS) resolve$(OBJSUFFICS) sql$(OBJSUFFICS) conf$(OBJSUFFICS) log$(OBJSUFFICS) datatypes$(OBJSUFFICS) md4$(OBJSUFFICS) md5$(OBJSUFFICS) blake2$(OBJSUFFICS) 3proxy_crypt$(OBJSUFFICS) base64$(OBJSUFFICS) ftp$(OBJSUFFICS) stringtable$(OBJSUFFICS) $(HTTPSRV_OBJS) srvdnspr$(OBJSUFFICS) plugins$(OBJSUFFICS) mdhash$(OBJSUFFICS) $(SSL_OBJS) $(PCRE_OBJS) $(TRANSPARENT_OBJS) $(COMPATLIBS) $(VERSIONDEP) + $(LN) $(LNOUT)$(BUILDDIR)3proxy$(EXESUFFICS) $(LDFLAGS) $(VERFILE) 3proxy$(OBJSUFFICS) loop$(OBJSUFFICS) fiber$(OBJSUFFICS) mainfunc$(OBJSUFFICS) auth$(OBJSUFFICS) acl$(OBJSUFFICS) limiter$(OBJSUFFICS) redirect$(OBJSUFFICS) authradius$(OBJSUFFICS) hash$(OBJSUFFICS) hashtables$(OBJSUFFICS) resolve$(OBJSUFFICS) sql$(OBJSUFFICS) conf$(OBJSUFFICS) datatypes$(OBJSUFFICS) srvauto$(OBJSUFFICS) srvproxy$(OBJSUFFICS) srvpop3p$(OBJSUFFICS) srvimapp$(OBJSUFFICS) srvsmtpp$(OBJSUFFICS) srvftppr$(OBJSUFFICS) srvsocks$(OBJSUFFICS) srvtcppm$(OBJSUFFICS) srvtlspr$(OBJSUFFICS) srvudppm$(OBJSUFFICS) sockmap$(OBJSUFFICS) udpsockmap$(OBJSUFFICS) sockgetchar$(OBJSUFFICS) common$(OBJSUFFICS) sem$(OBJSUFFICS) log$(OBJSUFFICS) 3proxy_crypt$(OBJSUFFICS) md4$(OBJSUFFICS) md5$(OBJSUFFICS) blake2$(OBJSUFFICS) base64$(OBJSUFFICS) ftp$(OBJSUFFICS) stringtable$(OBJSUFFICS) $(HTTPSRV_OBJS) srvdnspr$(OBJSUFFICS) plugins$(OBJSUFFICS) mdhash$(OBJSUFFICS) $(SSL_OBJS) $(PCRE_OBJS) $(TRANSPARENT_OBJS) $(COMPATLIBS) $(LIBS) $(PCRE_LIBS) diff --git a/src/fiber.c b/src/fiber.c new file mode 100644 index 0000000..8b6e037 --- /dev/null +++ b/src/fiber.c @@ -0,0 +1,474 @@ +/* + 3APA3A simplest proxy server + (c) 2002-2026 by Vladimir Dubrovin + + please read License Agreement +*/ + +#include "proxy.h" +#include "fiber.h" + +#ifdef WITH_FIBERS + +#ifndef _WIN32 +#include +#endif +#if defined FIBER_X86_64 && defined __linux__ +#include +#endif + +#if defined __has_feature +#if __has_feature(address_sanitizer) +#define FIBER_ASAN +#endif +#endif +#if defined __SANITIZE_ADDRESS__ && !defined FIBER_ASAN +#define FIBER_ASAN +#endif +#if defined FIBER_ASAN && defined FIBER_WINDOWS +#undef FIBER_ASAN +#endif +#ifdef FIBER_ASAN +#include +#endif + +#ifdef _MSC_VER +#define FTLS __declspec(thread) +#else +#define FTLS __thread +#endif + +/* the fiber running, NULL while the thread runs on its own stack */ +static FTLS struct fiber *current; +#ifdef FIBER_ASAN +/* the thread's own stack, for AddressSanitizer */ +static FTLS const void *threadbottom; +static FTLS size_t threadsize; +#endif + +static void fibermain(struct fiber *f); + + +/* ---- the switch ---------------------------------------------------------- + + fiberswap(from, to) saves the registers a function has to keep on the + stack, the stack pointer then to *from, and goes on from the stack + pointer to, where fiberswap() saved them, or fiberstart() put them for + a fiber not started: returning goes on with fiberentry(). */ + +#ifndef FIBER_WINDOWS + +#ifdef __APPLE__ +#define FSYM(x) "_" #x +#else +#define FSYM(x) #x +#endif +#if defined __ELF__ +#define FTYPE(x) ".type " FSYM(x) ",%function\n" +#define FSIZE(x) ".size " FSYM(x) ",.-" FSYM(x) "\n" +#else +#define FTYPE(x) +#define FSIZE(x) +#endif + +void fiberswap(void **from, void *to); + +#if defined FIBER_X86_64 +/* rbx, rbp, r12-r15 */ +__asm__( +".text\n" +".globl " FSYM(fiberswap) "\n" +FTYPE(fiberswap) +FSYM(fiberswap) ":\n" +" pushq %rbp\n" +" pushq %rbx\n" +" pushq %r12\n" +" pushq %r13\n" +" pushq %r14\n" +" pushq %r15\n" +" movq %rsp, (%rdi)\n" +" movq %rsi, %rsp\n" +" popq %r15\n" +" popq %r14\n" +" popq %r13\n" +" popq %r12\n" +" popq %rbx\n" +" popq %rbp\n" +" ret\n" +FSIZE(fiberswap) +); +#elif defined FIBER_AARCH64 +/* x19-x28, x29 (frame), x30 (return), d8-d15 */ +__asm__( +".text\n" +".globl " FSYM(fiberswap) "\n" +".p2align 2\n" +FTYPE(fiberswap) +FSYM(fiberswap) ":\n" +" sub sp, sp, #160\n" +" stp x19, x20, [sp, #0]\n" +" stp x21, x22, [sp, #16]\n" +" stp x23, x24, [sp, #32]\n" +" stp x25, x26, [sp, #48]\n" +" stp x27, x28, [sp, #64]\n" +" stp x29, x30, [sp, #80]\n" +" stp d8, d9, [sp, #96]\n" +" stp d10, d11, [sp, #112]\n" +" stp d12, d13, [sp, #128]\n" +" stp d14, d15, [sp, #144]\n" +" mov x2, sp\n" +" str x2, [x0]\n" +" mov sp, x1\n" +" ldp x19, x20, [sp, #0]\n" +" ldp x21, x22, [sp, #16]\n" +" ldp x23, x24, [sp, #32]\n" +" ldp x25, x26, [sp, #48]\n" +" ldp x27, x28, [sp, #64]\n" +" ldp x29, x30, [sp, #80]\n" +" ldp d8, d9, [sp, #96]\n" +" ldp d10, d11, [sp, #112]\n" +" ldp d12, d13, [sp, #128]\n" +" ldp d14, d15, [sp, #144]\n" +" add sp, sp, #160\n" +" ret\n" +FSIZE(fiberswap) +); +#elif defined FIBER_I386 +/* ebx, esi, edi, ebp; the arguments on the stack */ +__asm__( +".text\n" +".globl " FSYM(fiberswap) "\n" +FTYPE(fiberswap) +FSYM(fiberswap) ":\n" +" movl 4(%esp), %eax\n" +" movl 8(%esp), %edx\n" +" pushl %ebp\n" +" pushl %ebx\n" +" pushl %esi\n" +" pushl %edi\n" +" movl %esp, (%eax)\n" +" movl %edx, %esp\n" +" popl %edi\n" +" popl %esi\n" +" popl %ebx\n" +" popl %ebp\n" +" ret\n" +FSIZE(fiberswap) +); +#elif defined FIBER_ARM +/* r4-r11, lr, and d8-d15 where there is a floating point unit; in ARM + state, bx returning to Thumb code as well */ +#ifdef __ARM_FP +#define FARMVFP 1 +#define FARMVPUSH " vpush {d8-d15}\n" +#define FARMVPOP " vpop {d8-d15}\n" +#else +#define FARMVFP 0 +#define FARMVPUSH +#define FARMVPOP +#endif +__asm__( +".syntax unified\n" +".text\n" +".arm\n" +".globl " FSYM(fiberswap) "\n" +".p2align 2\n" +FTYPE(fiberswap) +FSYM(fiberswap) ":\n" +" push {r4-r11, lr}\n" +FARMVPUSH +" mov r2, sp\n" +" str r2, [r0]\n" +" mov sp, r1\n" +FARMVPOP +" pop {r4-r11, lr}\n" +" bx lr\n" +FSIZE(fiberswap) +#ifdef __thumb__ +".thumb\n" +#endif +); +#elif defined FIBER_MIPS +/* s0-s7, fp, gp, ra, and f20-f30 (f20-f31 with 64 bit registers) unless + floating point is soft. A fiber starts at fibertramp, which calls + fiberentry() through t9, as position independent code wants. */ +#if defined __mips_soft_float +#define FMIPSFP "" +#elif defined __mips_fpr && __mips_fpr == 64 +#define FMIPSFP(op) \ + " " op " $f20, 48($sp)\n " op " $f21, 56($sp)\n " op " $f22, 64($sp)\n" \ + " " op " $f23, 72($sp)\n " op " $f24, 80($sp)\n " op " $f25, 88($sp)\n" \ + " " op " $f26, 96($sp)\n " op " $f27, 104($sp)\n " op " $f28, 112($sp)\n" \ + " " op " $f29, 120($sp)\n " op " $f30, 128($sp)\n " op " $f31, 136($sp)\n" +#else +#define FMIPSFP(op) \ + " " op " $f20, 48($sp)\n " op " $f22, 56($sp)\n " op " $f24, 64($sp)\n" \ + " " op " $f26, 72($sp)\n " op " $f28, 80($sp)\n " op " $f30, 88($sp)\n" +#endif +#ifdef __mips_soft_float +#define FMIPSSAVE "" +#define FMIPSLOAD "" +#else +#define FMIPSSAVE FMIPSFP("sdc1") +#define FMIPSLOAD FMIPSFP("ldc1") +#endif +#define FMIPSFRAME 144 +__asm__( +".text\n" +".set push\n" +".set noreorder\n" +".set nomacro\n" +".globl fiberswap\n" +".ent fiberswap\n" +".type fiberswap,%function\n" +"fiberswap:\n" +" addiu $sp, $sp, -144\n" +" sw $16, 0($sp)\n" +" sw $17, 4($sp)\n" +" sw $18, 8($sp)\n" +" sw $19, 12($sp)\n" +" sw $20, 16($sp)\n" +" sw $21, 20($sp)\n" +" sw $22, 24($sp)\n" +" sw $23, 28($sp)\n" +" sw $30, 32($sp)\n" +" sw $28, 36($sp)\n" +" sw $31, 40($sp)\n" +FMIPSSAVE +" sw $sp, 0($4)\n" +" move $sp, $5\n" +" lw $16, 0($sp)\n" +" lw $17, 4($sp)\n" +" lw $18, 8($sp)\n" +" lw $19, 12($sp)\n" +" lw $20, 16($sp)\n" +" lw $21, 20($sp)\n" +" lw $22, 24($sp)\n" +" lw $23, 28($sp)\n" +" lw $30, 32($sp)\n" +" lw $28, 36($sp)\n" +" lw $31, 40($sp)\n" +FMIPSLOAD +" jr $31\n" +" addiu $sp, $sp, 144\n" +".end fiberswap\n" +".size fiberswap,.-fiberswap\n" +".globl fibertramp\n" +".ent fibertramp\n" +".type fibertramp,%function\n" +"fibertramp:\n" +" move $25, $16\n" +" jalr $25\n" +" nop\n" +".end fibertramp\n" +".size fibertramp,.-fibertramp\n" +".set pop\n" +); +void fibertramp(void); +#endif + +/* where a fiber starts: on its own stack, with nothing to return to */ +static void fiberentry(void){ + fibermain(current); +} + +/* what the thread was doing while a fiber runs */ +static FTLS void *threadsp; + +#endif /* !FIBER_WINDOWS */ + +#ifdef FIBER_WINDOWS +#ifndef FIBER_FLAG_FLOAT_SWITCH +#define FIBER_FLAG_FLOAT_SWITCH 1 +#endif +/* the thread as a fiber, to switch back to */ +static FTLS void *threadfiber; + +static VOID WINAPI fiberproc(LPVOID arg){ + fibermain((struct fiber *)arg); +} +#endif + + +/* ---- fibers ----------------------------------------------------------------- */ + +/* 0 where fibers cannot be switched to: a shadow stack, checked by the + processor against each return, which ours would not match. */ +int fiberusable(void){ +#if defined FIBER_X86_64 && defined __linux__ && defined SYS_arch_prctl + unsigned long features = 0; + + /* ARCH_SHSTK_STATUS, ARCH_SHSTK_SHSTK */ + if(!syscall(SYS_arch_prctl, 0x5005, &features) && (features & 1)) return 0; +#endif + return 1; +} + +int fiberinit(struct fiber *f, size_t stacksize){ + memset(f, 0, sizeof(*f)); +#ifdef FIBER_WINDOWS + f->handle = CreateFiberEx(0, stacksize, FIBER_FLAG_FLOAT_SWITCH, fiberproc, f); + return !f->handle; +#else + { + size_t page = (size_t)sysconf(_SC_PAGESIZE); + int flags = MAP_PRIVATE; + void *p; + +#ifdef MAP_ANONYMOUS + flags |= MAP_ANONYMOUS; +#else + flags |= MAP_ANON; +#endif +#ifdef MAP_STACK + flags |= MAP_STACK; +#endif + stacksize = (stacksize + page - 1) & ~(page - 1); + f->mapped = stacksize + page; + p = mmap(NULL, f->mapped, PROT_READ | PROT_WRITE, flags, -1, 0); + if(p == MAP_FAILED) return 1; + f->map = p; + /* running over the bottom faults rather than overwriting */ + if(mprotect(f->map, page, PROT_NONE)){ + munmap(f->map, f->mapped); + return 1; + } + } + return 0; +#endif +} + +/* A fiber neither running nor in the middle of func. */ +void fiberfree(struct fiber *f){ +#ifdef FIBER_WINDOWS + if(f->handle) DeleteFiber(f->handle); +#else + if(f->map) munmap(f->map, f->mapped); +#endif + memset(f, 0, sizeof(*f)); +} + +/* func(arg) is what f runs once resumed: f is new, or func of its last + start returned. */ +void fiberstart(struct fiber *f, void (*func)(void *arg), void *arg){ + f->func = func; + f->arg = arg; + if(f->started) return; + f->started = 1; +#ifndef FIBER_WINDOWS + { + uintptr_t top = (uintptr_t)(f->map + f->mapped) & ~(uintptr_t)15; + void **sp; + +#if defined FIBER_X86_64 + sp = (void **)top; + *--sp = NULL; /* fiberentry() returns nowhere */ + *--sp = (void *)fiberentry; /* where fiberswap() returns */ + sp -= 6; + memset(sp, 0, 6 * sizeof(*sp)); +#elif defined FIBER_AARCH64 + sp = (void **)top - 20; + memset(sp, 0, 20 * sizeof(*sp)); + sp[11] = (void *)fiberentry; /* x30 */ +#elif defined FIBER_I386 + sp = (void **)top; + *--sp = NULL; + *--sp = (void *)fiberentry; + sp -= 4; + memset(sp, 0, 4 * sizeof(*sp)); +#elif defined FIBER_ARM + { + int n = (FARMVFP? 16 : 0) + 9; + + sp = (void **)top - n; + memset(sp, 0, n * sizeof(*sp)); + sp[n - 1] = (void *)fiberentry; /* lr */ + } +#elif defined FIBER_MIPS + { + void *gp; + + /* room for the arguments fiberentry() may store above it */ + sp = (void **)(top - 32) - FMIPSFRAME / sizeof(void *); + memset(sp, 0, FMIPSFRAME); + __asm__ volatile ("move %0, $28" : "=r"(gp)); + sp[0] = (void *)fiberentry; /* s0, called by fibertramp */ + sp[9] = gp; + sp[10] = (void *)fibertramp; /* ra */ + } +#endif + f->sp = sp; + } +#endif +} + +/* Runs f until it yields or func returns: from the thread's own stack. + 1 while func has not returned. */ +int fiberresume(struct fiber *f){ +#ifdef FIBER_ASAN + void *fake = NULL; +#endif + + current = f; +#ifdef FIBER_ASAN + { + size_t page = (size_t)sysconf(_SC_PAGESIZE); + + __sanitizer_start_switch_fiber(&fake, f->map + page, f->mapped - page); + } +#endif +#ifdef FIBER_WINDOWS + if(!threadfiber){ + if(!(threadfiber = ConvertThreadToFiber(NULL)) && GetLastError() == ERROR_ALREADY_FIBER) + threadfiber = GetCurrentFiber(); + if(!threadfiber){ + current = NULL; + return 1; + } + } + SwitchToFiber(f->handle); +#else + fiberswap(&threadsp, f->sp); +#endif +#ifdef FIBER_ASAN + __sanitizer_finish_switch_fiber(fake, NULL, NULL); +#endif + current = NULL; + return f->func != NULL; +} + +/* From a fiber: back to the thread, until it is resumed. */ +void fiberyield(void){ + struct fiber *f = current; + +#ifdef FIBER_ASAN + __sanitizer_start_switch_fiber(&f->fake, threadbottom, threadsize); +#endif +#ifdef FIBER_WINDOWS + SwitchToFiber(threadfiber); +#else + fiberswap(&f->sp, threadsp); +#endif +#ifdef FIBER_ASAN + __sanitizer_finish_switch_fiber(f->fake, &threadbottom, &threadsize); +#endif +} + +struct fiber *fibercurrent(void){ + return current; +} + +/* What a fiber runs: func each time it is started, yielding once it + returns. */ +static void fibermain(struct fiber *f){ +#ifdef FIBER_ASAN + __sanitizer_finish_switch_fiber(NULL, &threadbottom, &threadsize); +#endif + for(;;){ + f->func(f->arg); + f->func = NULL; + fiberyield(); + } +} + +#endif diff --git a/src/fiber.h b/src/fiber.h new file mode 100644 index 0000000..90eb625 --- /dev/null +++ b/src/fiber.h @@ -0,0 +1,60 @@ +/* + 3APA3A simplest proxy server + (c) 2002-2026 by Vladimir Dubrovin + + please read License Agreement +*/ + +/* Fibers: stacks of their own a thread switches to and back, for a thread + to serve many clients each with code written as if it had a thread of + its own. The switch is our own where the processor is one of ours, + Windows' fibers on Windows; elsewhere there are none (WITH_FIBERS is not + defined), and each client has a thread. A fiber is resumed by the thread + it belongs to and yields back to it: fibers do not switch to each other, + nor move between threads. */ + +#ifndef _FIBER_H +#define _FIBER_H + +#if defined _WIN32 +#if !defined __WATCOMC__ && !defined _WINCE +#define FIBER_WINDOWS +#endif +#elif defined __x86_64__ && defined __LP64__ +#define FIBER_X86_64 +#elif defined __aarch64__ && defined __LP64__ +#define FIBER_AARCH64 +#elif defined __i386__ +#define FIBER_I386 +#elif defined __arm__ +#define FIBER_ARM +#elif defined __mips__ && defined _MIPS_SIM && defined _ABIO32 && _MIPS_SIM == _ABIO32 +#define FIBER_MIPS +#endif + +#if defined FIBER_WINDOWS || defined FIBER_X86_64 || defined FIBER_AARCH64 || defined FIBER_I386 || defined FIBER_ARM || defined FIBER_MIPS +#define WITH_FIBERS + +struct fiber { + void *sp; /* where its registers are while it does not run */ + unsigned char *map; /* its stack, the guard page at the bottom */ + size_t mapped; + void (*func)(void *arg); /* what it runs, NULL once returned */ + void *arg; + int started; +#ifdef FIBER_WINDOWS + void *handle; +#endif + void *fake; /* AddressSanitizer's, while it does not run */ +}; + +int fiberusable(void); +int fiberinit(struct fiber *f, size_t stacksize); +void fiberfree(struct fiber *f); +void fiberstart(struct fiber *f, void (*func)(void *arg), void *arg); +int fiberresume(struct fiber *f); +void fiberyield(void); +struct fiber *fibercurrent(void); + +#endif +#endif diff --git a/tests/README.md b/tests/README.md index f146eb1..59664e9 100644 --- a/tests/README.md +++ b/tests/README.md @@ -68,10 +68,13 @@ Note that access rules accumulate until `flush`, so a service section that means to stand on its own should start with one - otherwise an earlier `allow *` matches first and the rule under test is never reached. -The event loop (`src/loop.c`) is tested on its own by `tests/looptest.c`, -which CMake builds next to the 3proxy binary and the `loop` case runs: timers -on a clock of its own, through several turns of the wheel, sockets, and -other threads. Where it was not built the case is skipped. +The event loop (`src/loop.c`) and fibers (`src/fiber.c`) are tested on their +own by `tests/looptest.c` and `tests/fibertest.c`, which CMake builds next to +the 3proxy binary and the `units` case runs: timers on a clock of their own, +through several turns of the wheel, sockets, other threads; fibers yielding +and resumed, the registers a function keeps, a thousand stacks, fibers +waiting on the loop's timers. Where they were not built the case is +skipped. ## What is not covered yet diff --git a/tests/cases/loop.py b/tests/cases/loop.py deleted file mode 100644 index 328830b..0000000 --- a/tests/cases/loop.py +++ /dev/null @@ -1,30 +0,0 @@ -"""The event loop on its own: tests/looptest.c, built by CMake next to the -3proxy binary, run with a few seeds for its timers. Skipped where it was not -built (the Makefiles do not build it).""" - -import os -import subprocess - - -def run(t): - name = "looptest.exe" if os.name == "nt" else "looptest" - path = os.path.join(os.path.dirname(os.path.abspath(t.binary)), name) - if not os.path.exists(path): - t.skip("looptest is built by CMake only") - return - for seed in ("1", "2", "3"): - try: - done = subprocess.run([path, seed], stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, timeout=120) - except subprocess.TimeoutExpired: - t.fail(f"looptest {seed} finishes") - continue - out = done.stdout.decode(errors="replace") - for line in out.splitlines(): - if line.startswith("ok "): - t.ok(f"seed {seed}: {line[3:]}") - elif line.startswith("FAIL "): - label, _, what = line[5:].partition(": ") - t.fail(f"seed {seed}: {label}", actual=what) - if done.returncode and "FAIL " not in out: - t.fail(f"looptest {seed} exits cleanly", 0, done.returncode) diff --git a/tests/cases/units.py b/tests/cases/units.py new file mode 100644 index 0000000..515f06b --- /dev/null +++ b/tests/cases/units.py @@ -0,0 +1,32 @@ +"""The event loop and fibers on their own: tests/looptest.c and +tests/fibertest.c, built by CMake next to the 3proxy binary, each run with +a few seeds. Skipped where they were not built (the Makefiles do not build +them).""" + +import os +import subprocess + + +def run(t): + here = os.path.dirname(os.path.abspath(t.binary)) + for unit in ("looptest", "fibertest"): + path = os.path.join(here, unit + (".exe" if os.name == "nt" else "")) + if not os.path.exists(path): + t.skip(f"{unit} is built by CMake only") + continue + for seed in ("1", "2", "3"): + try: + done = subprocess.run([path, seed], stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=120) + except subprocess.TimeoutExpired: + t.fail(f"{unit} {seed} finishes") + continue + out = done.stdout.decode(errors="replace") + for line in out.splitlines(): + if line.startswith("ok "): + t.ok(f"{unit} {seed}: {line[3:]}") + elif line.startswith("FAIL "): + label, _, what = line[5:].partition(": ") + t.fail(f"{unit} {seed}: {label}", actual=what) + if done.returncode and "FAIL " not in out: + t.fail(f"{unit} {seed} exits cleanly", 0, done.returncode) diff --git a/tests/fibertest.c b/tests/fibertest.c new file mode 100644 index 0000000..71091c8 --- /dev/null +++ b/tests/fibertest.c @@ -0,0 +1,321 @@ +/* + 3APA3A simplest proxy server + (c) 2002-2026 by Vladimir Dubrovin + + please read License Agreement +*/ + +/* Fibers on their own, and waiting on the event loop: tests/cases/units.py + runs it. Each check is a line, "ok LABEL" or "FAIL LABEL: what", the + exit code the number of failures. */ + +#include "../src/proxy.h" +#include "../src/loop.h" +#include "../src/fiber.h" + +static int failures; + +static void check(int cond, const char *label, const char *what){ + if(cond) printf("ok %s\n", label); + else { + printf("FAIL %s: %s\n", label, what); + failures++; + } + fflush(stdout); +} + +#ifndef WITH_FIBERS + +int main(void){ + printf("ok no fibers on this processor: each client has a thread\n"); + return 0; +} + +#else + +#define STACK 65536 + +#ifdef _MSC_VER +#define NOINLINE __declspec(noinline) +#else +#define NOINLINE __attribute__((noinline)) +#endif + +static unsigned long long rnd = 88172645463325252ULL; + +static unsigned long long rand64(void){ + rnd ^= rnd << 13; + rnd ^= rnd >> 7; + rnd ^= rnd << 17; + return rnd; +} + + +/* ---- one fiber, yielding ------------------------------------------------ */ + +static int steps[8], nsteps; +static struct fiber *seen; + +static void stepper(void *arg){ + int i; + + seen = fibercurrent(); + for(i = 0; i < 5; i++){ + steps[nsteps++] = i + *(int *)arg; + fiberyield(); + } +} + +static void basic(void){ + struct fiber f; + int base = 100, i, more = 0, ok = 1; + char what[128]; + + check(fibercurrent() == NULL, "the thread runs on its own stack", "a fiber is current"); + if(fiberinit(&f, STACK)){ + check(0, "a fiber is made", "fiberinit() failed"); + return; + } + fiberstart(&f, stepper, &base); + for(i = 0; i < 6; i++){ + more = fiberresume(&f); + if(i < 5 && !more) ok = 0; + } + check(seen == &f, "a fiber is current while it runs", "it is not"); + check(fibercurrent() == NULL, "the thread is back on its own stack", "a fiber is current"); + for(i = 0; i < 5; i++) if(steps[i] != 100 + i) ok = 0; + sprintf(what, "%d steps, more %d", nsteps, more); + check(ok && nsteps == 5 && !more, "it runs to each yield in turn, then returns", what); + + /* started again once it returned */ + nsteps = 0; + base = 200; + fiberstart(&f, stepper, &base); + while(fiberresume(&f)); + check(nsteps == 5 && steps[4] == 204, "a fiber is started again", "it is not"); + fiberfree(&f); +} + + +/* ---- what a function has to keep, kept ---------------------------------- */ + +/* Many values live across the switch: the compiler keeps them in the + registers a call has to preserve. */ +static NOINLINE unsigned long churn(unsigned long seed, double *dout, + int (*sw)(void *), void *swarg){ + unsigned long a = seed, b = seed * 3, c = seed * 5, d = seed * 7, e = seed * 11, + g = seed * 13, h = seed * 17, k = seed * 19; + double x = (double)seed / 3, y = (double)seed / 7, z = (double)seed * 1.5, + w = (double)seed + 0.25, u = (double)seed * 0.125, v = (double)seed - 0.5, + q = (double)seed * 2.75, r = (double)seed / 9; + int i; + + for(i = 0; i < 4; i++){ + a += b ^ c; b += c ^ d; c += d ^ e; d += e ^ g; e += g ^ h; g += h ^ k; h += k ^ a; k += a ^ b; + x = x * 1.0001 + y; y = y * 0.9999 + z; z = z * 1.0002 - w; w = w * 0.9998 + u; + u = u * 1.0003 + v; v = v * 0.9997 - q; q = q * 1.0004 + r; r = r * 0.9996 + x; + sw(swarg); + } + *dout = x + y + z + w + u + v + q + r; + return a ^ b ^ c ^ d ^ e ^ g ^ h ^ k; +} + +static int noswitch(void *arg){ + return 0; +} + +static int doyield(void *arg){ + fiberyield(); + return 0; +} + +static int doresume(void *arg){ + return fiberresume((struct fiber *)arg); +} + +static unsigned long fibresult; +static double fibdouble; + +static void churner(void *arg){ + fibresult = churn(12345, &fibdouble, doyield, NULL); +} + +static void kept(void){ + struct fiber f, g; + unsigned long expect, got; + double dexpect, dgot; + + if(fiberinit(&f, STACK) || fiberinit(&g, STACK)){ + check(0, "fibers are made", "fiberinit() failed"); + return; + } + expect = churn(12345, &dexpect, noswitch, NULL); + + /* the fiber's values across its yields */ + fiberstart(&f, churner, NULL); + while(fiberresume(&f)); + check(fibresult == expect && fibdouble == dexpect, + "a fiber's integer and floating point values survive its yields", "they changed"); + + /* the thread's values across the fiber running */ + fiberstart(&g, churner, NULL); + got = churn(12345, &dgot, doresume, &g); + while(fiberresume(&g)); + check(got == expect && dgot == dexpect, + "the thread's integer and floating point values survive a fiber running", "they changed"); + fiberfree(&f); + fiberfree(&g); +} + + +/* ---- many fibers, their stacks their own -------------------------------- */ + +#define NFIB 1000 +#define ROUNDS 20 + +static int corrupted; + +static void filler(void *arg){ + unsigned id = (unsigned)(uintptr_t)arg; + unsigned char buf[4096]; + int r; + size_t i; + + for(r = 0; r < ROUNDS; r++){ + for(i = 0; i < sizeof(buf); i++) buf[i] = (unsigned char)(id * 31 + r * 7 + i); + fiberyield(); + for(i = 0; i < sizeof(buf); i++) if(buf[i] != (unsigned char)(id * 31 + r * 7 + i)){ + corrupted++; + break; + } + } +} + +static void many(void){ + static struct fiber f[NFIB]; + static int done[NFIB]; + int i, left = NFIB; + char what[64]; + + for(i = 0; i < NFIB; i++){ + if(fiberinit(f + i, STACK)){ + check(0, "a thousand fibers are made", "fiberinit() failed"); + while(i--) fiberfree(f + i); + return; + } + fiberstart(f + i, filler, (void *)(uintptr_t)i); + } + while(left){ + i = (int)(rand64() % NFIB); + if(done[i]) continue; + if(!fiberresume(f + i)){ + done[i] = 1; + left--; + } + } + sprintf(what, "%d stacks changed under their fiber", corrupted); + check(!corrupted, "a thousand fibers resumed at random keep their stacks", what); + for(i = 0; i < NFIB; i++) fiberfree(f + i); +} + + +/* ---- deep enough ------------------------------------------------------------ */ + +static NOINLINE unsigned deep(unsigned n){ + volatile unsigned char pad[256]; + + pad[0] = (unsigned char)n; + pad[255] = (unsigned char)(n >> 1); + if(!n) return 0; + return pad[0] + pad[255] + deep(n - 1); +} + +static unsigned deepresult; + +static void deeper(void *arg){ + fiberyield(); + deepresult = deep(150); +} + +static void depth(void){ + struct fiber f; + + if(fiberinit(&f, STACK)) return; + fiberstart(&f, deeper, NULL); + while(fiberresume(&f)); + check(deepresult == deep(150), "a fiber uses most of its stack", "wrong result"); + fiberfree(&f); +} + + +/* ---- fibers waiting on the event loop ------------------------------------ */ + +#define NWAIT 100 + +struct waiter { + struct fiber f; + struct looptimer t; + struct loop *l; + unsigned ms; + uint64_t slept; + int done; +}; + +static void wake(struct loop *l, struct looptimer *t){ + struct waiter *w = t->data; + + if(!fiberresume(&w->f)) w->done = 1; +} + +/* what a client's code will do: wait as if it had a thread of its own */ +static void sleeper(void *arg){ + struct waiter *w = arg; + uint64_t start = looptime(); + + looptimerset(w->l, &w->t, start + w->ms); + fiberyield(); + w->slept = looptime() - start; +} + +static void onloop(void){ + static struct waiter w[NWAIT]; + struct loop l; + int i, left, early = 0; + char what[64]; + + if(loopinit(&l)) return; + for(i = 0; i < NWAIT; i++){ + memset(w + i, 0, sizeof(w[i])); + if(fiberinit(&w[i].f, STACK)) return; + w[i].l = &l; + w[i].ms = 10 + (unsigned)(rand64() % 300); + w[i].t.func = wake; + w[i].t.data = w + i; + fiberstart(&w[i].f, sleeper, w + i); + fiberresume(&w[i].f); + } + for(left = NWAIT; left; ){ + looponce(&l, -1); + for(left = 0, i = 0; i < NWAIT; i++) if(!w[i].done) left++; + } + for(i = 0; i < NWAIT; i++) if(w[i].slept < w[i].ms) early++; + sprintf(what, "%d woke early", early); + check(!early, "a hundred fibers sleep on the loop's timers, each as long as it asked", what); + for(i = 0; i < NWAIT; i++) fiberfree(&w[i].f); + loopfree(&l); +} + +int main(int argc, char **argv){ + if(!fiberusable()){ + printf("ok fibers cannot be used here (a shadow stack): each client has a thread\n"); + return 0; + } + basic(); + kept(); + many(); + depth(); + onloop(); + return failures; +} + +#endif