From 29d57ca4f1f7349d4f2d6a0cc0e5f5ff682a4a92 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 21:45:27 +0000 Subject: [PATCH 1/6] feat(libstlx): add futex, mutex, condvar, and barrier C primitives Futex wrappers for SYS_FUTEX_WAIT/WAKE/WAKE_ALL syscalls. Three-state mutex (Drepper algorithm) with atomic fast path. Sequence-counter condition variable. Generation-counter reusable barrier. Co-authored-by: Albert Slepak --- userland/lib/libstlx/include/stlx/barrier.h | 17 +++++++++ userland/lib/libstlx/include/stlx/cond.h | 21 +++++++++++ userland/lib/libstlx/include/stlx/futex.h | 16 +++++++++ userland/lib/libstlx/include/stlx/mutex.h | 17 +++++++++ .../lib/libstlx/include/stlx/syscall_nums.h | 4 +++ userland/lib/libstlx/src/barrier.c | 23 ++++++++++++ userland/lib/libstlx/src/cond.c | 19 ++++++++++ userland/lib/libstlx/src/futex.c | 16 +++++++++ userland/lib/libstlx/src/mutex.c | 35 +++++++++++++++++++ 9 files changed, 168 insertions(+) create mode 100644 userland/lib/libstlx/include/stlx/barrier.h create mode 100644 userland/lib/libstlx/include/stlx/cond.h create mode 100644 userland/lib/libstlx/include/stlx/futex.h create mode 100644 userland/lib/libstlx/include/stlx/mutex.h create mode 100644 userland/lib/libstlx/src/barrier.c create mode 100644 userland/lib/libstlx/src/cond.c create mode 100644 userland/lib/libstlx/src/futex.c create mode 100644 userland/lib/libstlx/src/mutex.c diff --git a/userland/lib/libstlx/include/stlx/barrier.h b/userland/lib/libstlx/include/stlx/barrier.h new file mode 100644 index 00000000..082fab7f --- /dev/null +++ b/userland/lib/libstlx/include/stlx/barrier.h @@ -0,0 +1,17 @@ +#ifndef STLX_BARRIER_H +#define STLX_BARRIER_H + +#include + +typedef struct { + uint32_t count; + uint32_t generation; + uint32_t total; +} stlx_barrier_t; + +void stlx_barrier_init(stlx_barrier_t* b, uint32_t count); + +/* Block until all count threads have called barrier_wait. Reusable. */ +void stlx_barrier_wait(stlx_barrier_t* b); + +#endif /* STLX_BARRIER_H */ diff --git a/userland/lib/libstlx/include/stlx/cond.h b/userland/lib/libstlx/include/stlx/cond.h new file mode 100644 index 00000000..f5740000 --- /dev/null +++ b/userland/lib/libstlx/include/stlx/cond.h @@ -0,0 +1,21 @@ +#ifndef STLX_COND_H +#define STLX_COND_H + +#include +#include + +typedef struct { uint32_t seq; } stlx_cond_t; + +#define STLX_COND_INIT { 0 } + +/* Atomically unlocks the mutex and sleeps until signaled, then re-locks. + * Callers must re-check the predicate in a loop (spurious wakeups allowed). */ +void stlx_cond_wait(stlx_cond_t* cv, stlx_mutex_t* m); + +/* Wake one waiter. */ +void stlx_cond_signal(stlx_cond_t* cv); + +/* Wake all waiters. */ +void stlx_cond_broadcast(stlx_cond_t* cv); + +#endif /* STLX_COND_H */ diff --git a/userland/lib/libstlx/include/stlx/futex.h b/userland/lib/libstlx/include/stlx/futex.h new file mode 100644 index 00000000..bb124e8f --- /dev/null +++ b/userland/lib/libstlx/include/stlx/futex.h @@ -0,0 +1,16 @@ +#ifndef STLX_FUTEX_H +#define STLX_FUTEX_H + +#include + +/* Block if *addr == expected. timeout_ns=0 waits indefinitely. + * Returns 0 on wake, negative errno on error (-EAGAIN, -ETIMEDOUT). */ +int stlx_futex_wait(uint32_t* addr, uint32_t expected, uint64_t timeout_ns); + +/* Wake up to count threads waiting on addr. Returns number woken. */ +int stlx_futex_wake(uint32_t* addr, uint32_t count); + +/* Wake all threads waiting on addr. Returns number woken. */ +int stlx_futex_wake_all(uint32_t* addr); + +#endif /* STLX_FUTEX_H */ diff --git a/userland/lib/libstlx/include/stlx/mutex.h b/userland/lib/libstlx/include/stlx/mutex.h new file mode 100644 index 00000000..b5e87f4a --- /dev/null +++ b/userland/lib/libstlx/include/stlx/mutex.h @@ -0,0 +1,17 @@ +#ifndef STLX_MUTEX_H +#define STLX_MUTEX_H + +#include + +/* State 0: unlocked, 1: locked (no waiters), 2: locked (with waiters) */ +typedef struct { uint32_t state; } stlx_mutex_t; + +#define STLX_MUTEX_INIT { 0 } + +void stlx_mutex_lock(stlx_mutex_t* m); +void stlx_mutex_unlock(stlx_mutex_t* m); + +/* Returns 0 if acquired, -1 if already held. */ +int stlx_mutex_trylock(stlx_mutex_t* m); + +#endif /* STLX_MUTEX_H */ diff --git a/userland/lib/libstlx/include/stlx/syscall_nums.h b/userland/lib/libstlx/include/stlx/syscall_nums.h index f6cc7590..b4b07213 100644 --- a/userland/lib/libstlx/include/stlx/syscall_nums.h +++ b/userland/lib/libstlx/include/stlx/syscall_nums.h @@ -12,4 +12,8 @@ #define SYS_PROC_KILL_TID 1018 #define SYS_PTY_CREATE 1020 +#define SYS_FUTEX_WAIT 1030 +#define SYS_FUTEX_WAKE 1031 +#define SYS_FUTEX_WAKE_ALL 1032 + #endif /* STLX_SYSCALL_NUMS_H */ diff --git a/userland/lib/libstlx/src/barrier.c b/userland/lib/libstlx/src/barrier.c new file mode 100644 index 00000000..89d31d80 --- /dev/null +++ b/userland/lib/libstlx/src/barrier.c @@ -0,0 +1,23 @@ +#include +#include + +void stlx_barrier_init(stlx_barrier_t* b, uint32_t count) { + b->count = 0; + b->generation = 0; + b->total = count; +} + +void stlx_barrier_wait(stlx_barrier_t* b) { + uint32_t gen = __atomic_load_n(&b->generation, __ATOMIC_ACQUIRE); + uint32_t arrived = __atomic_fetch_add(&b->count, 1, __ATOMIC_ACQ_REL) + 1; + + if (arrived == b->total) { + __atomic_store_n(&b->count, 0, __ATOMIC_RELAXED); + __atomic_fetch_add(&b->generation, 1, __ATOMIC_RELEASE); + stlx_futex_wake_all(&b->generation); + } else { + while (__atomic_load_n(&b->generation, __ATOMIC_ACQUIRE) == gen) { + stlx_futex_wait(&b->generation, gen, 0); + } + } +} diff --git a/userland/lib/libstlx/src/cond.c b/userland/lib/libstlx/src/cond.c new file mode 100644 index 00000000..29c28399 --- /dev/null +++ b/userland/lib/libstlx/src/cond.c @@ -0,0 +1,19 @@ +#include +#include + +void stlx_cond_wait(stlx_cond_t* cv, stlx_mutex_t* m) { + uint32_t seq = __atomic_load_n(&cv->seq, __ATOMIC_RELAXED); + stlx_mutex_unlock(m); + stlx_futex_wait(&cv->seq, seq, 0); + stlx_mutex_lock(m); +} + +void stlx_cond_signal(stlx_cond_t* cv) { + __atomic_fetch_add(&cv->seq, 1, __ATOMIC_RELEASE); + stlx_futex_wake(&cv->seq, 1); +} + +void stlx_cond_broadcast(stlx_cond_t* cv) { + __atomic_fetch_add(&cv->seq, 1, __ATOMIC_RELEASE); + stlx_futex_wake_all(&cv->seq); +} diff --git a/userland/lib/libstlx/src/futex.c b/userland/lib/libstlx/src/futex.c new file mode 100644 index 00000000..2645aeed --- /dev/null +++ b/userland/lib/libstlx/src/futex.c @@ -0,0 +1,16 @@ +#define _GNU_SOURCE +#include +#include +#include + +int stlx_futex_wait(uint32_t* addr, uint32_t expected, uint64_t timeout_ns) { + return (int)syscall(SYS_FUTEX_WAIT, addr, expected, timeout_ns); +} + +int stlx_futex_wake(uint32_t* addr, uint32_t count) { + return (int)syscall(SYS_FUTEX_WAKE, addr, count); +} + +int stlx_futex_wake_all(uint32_t* addr) { + return (int)syscall(SYS_FUTEX_WAKE_ALL, addr); +} diff --git a/userland/lib/libstlx/src/mutex.c b/userland/lib/libstlx/src/mutex.c new file mode 100644 index 00000000..779e48be --- /dev/null +++ b/userland/lib/libstlx/src/mutex.c @@ -0,0 +1,35 @@ +#include +#include + +void stlx_mutex_lock(stlx_mutex_t* m) { + uint32_t c = 0; + if (__atomic_compare_exchange_n(&m->state, &c, 1, 0, + __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) { + return; + } + + do { + if (c == 2 || __atomic_compare_exchange_n(&m->state, &c, 2, 0, + __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + stlx_futex_wait(&m->state, 2, 0); + } + c = 0; + } while (!__atomic_compare_exchange_n(&m->state, &c, 2, 0, + __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)); +} + +void stlx_mutex_unlock(stlx_mutex_t* m) { + uint32_t prev = __atomic_exchange_n(&m->state, 0, __ATOMIC_RELEASE); + if (prev == 2) { + stlx_futex_wake(&m->state, 1); + } +} + +int stlx_mutex_trylock(stlx_mutex_t* m) { + uint32_t c = 0; + if (__atomic_compare_exchange_n(&m->state, &c, 1, 0, + __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) { + return 0; + } + return -1; +} From 79124e89cde9cd6c8af46da4866a5806ff93089c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 21:47:53 +0000 Subject: [PATCH 2/6] feat(libstlxcxx): add C++ threading library with thread, mutex, condvar, barrier stlxstd::thread - RAII thread with mmap'd stack, join/detach support stlxstd::mutex - wraps stlx_mutex_t with lock/unlock/try_lock stlxstd::lock_guard / unique_lock - RAII lock management stlxstd::condition_variable - wraps stlx_cond_t with wait/notify stlxstd::barrier - wraps stlx_barrier_t with arrive_and_wait Type-erased thread callable via function pointer dispatch (no RTTI). Co-authored-by: Albert Slepak --- userland/lib/Makefile | 2 +- userland/lib/libstlxcxx/Makefile | 47 +++++++ .../lib/libstlxcxx/include/stlxstd/barrier.h | 28 +++++ .../include/stlxstd/condition_variable.h | 35 ++++++ .../lib/libstlxcxx/include/stlxstd/mutex.h | 82 ++++++++++++ .../lib/libstlxcxx/include/stlxstd/thread.h | 117 ++++++++++++++++++ userland/lib/libstlxcxx/src/thread.cpp | 14 +++ 7 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 userland/lib/libstlxcxx/Makefile create mode 100644 userland/lib/libstlxcxx/include/stlxstd/barrier.h create mode 100644 userland/lib/libstlxcxx/include/stlxstd/condition_variable.h create mode 100644 userland/lib/libstlxcxx/include/stlxstd/mutex.h create mode 100644 userland/lib/libstlxcxx/include/stlxstd/thread.h create mode 100644 userland/lib/libstlxcxx/src/thread.cpp diff --git a/userland/lib/Makefile b/userland/lib/Makefile index 20506879..e8801107 100644 --- a/userland/lib/Makefile +++ b/userland/lib/Makefile @@ -2,7 +2,7 @@ # Stellux Userland - Libraries # -LIB_DIRS := libstlx libstlxgfx libbearssl +LIB_DIRS := libstlx libstlxcxx libstlxgfx libbearssl LIB_COUNT := $(words $(LIB_DIRS)) all: diff --git a/userland/lib/libstlxcxx/Makefile b/userland/lib/libstlxcxx/Makefile new file mode 100644 index 00000000..97f6ba08 --- /dev/null +++ b/userland/lib/libstlxcxx/Makefile @@ -0,0 +1,47 @@ +# +# Stellux Userland - libstlxcxx Static Library +# +# C++ threading wrappers (stlxstd::thread, mutex, etc.) built on libstlx. +# + +USERLAND_ROOT ?= $(shell cd $(dir $(lastword $(MAKEFILE_LIST)))/../.. && pwd) +include $(USERLAND_ROOT)/mk/toolchain.mk + +AR ?= llvm-ar + +SRC_DIR := src +INCLUDE_DIR := include +BUILD_DIR := build/$(ARCH) +LIB_NAME := libstlxcxx.a +TARGET := $(BUILD_DIR)/$(LIB_NAME) + +SYSROOT_LIB := $(USERLAND_ROOT)/sysroot/$(ARCH)/lib +SYSROOT_INC := $(USERLAND_ROOT)/sysroot/$(ARCH)/include/stlxstd + +SOURCES := $(wildcard $(SRC_DIR)/*.cpp) +OBJECTS := $(SOURCES:$(SRC_DIR)/%.cpp=$(BUILD_DIR)/%.o) + +all: $(TARGET) install-headers + +$(TARGET): $(OBJECTS) + $(UQ)mkdir -p $(dir $@) + @echo "[AR] libstlxcxx.a ($(ARCH))" + $(UQ)$(AR) crs $@ $(OBJECTS) + $(UQ)mkdir -p $(SYSROOT_LIB) + $(UQ)cp $@ $(SYSROOT_LIB)/ + +install-headers: + $(UQ)mkdir -p $(SYSROOT_INC) + $(UQ)cp $(INCLUDE_DIR)/stlxstd/*.h $(SYSROOT_INC)/ + +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp + $(UQ)mkdir -p $(dir $@) + @echo "[CXX] $< ($(ARCH))" + $(UQ)$(CXX) $(CXXFLAGS_COMMON) -I$(INCLUDE_DIR) -c $< -o $@ + +clean: + $(UQ)rm -rf build + $(UQ)rm -f $(SYSROOT_LIB)/$(LIB_NAME) + $(UQ)rm -rf $(SYSROOT_INC) + +.PHONY: all install-headers clean diff --git a/userland/lib/libstlxcxx/include/stlxstd/barrier.h b/userland/lib/libstlxcxx/include/stlxstd/barrier.h new file mode 100644 index 00000000..d3dba748 --- /dev/null +++ b/userland/lib/libstlxcxx/include/stlxstd/barrier.h @@ -0,0 +1,28 @@ +#ifndef STLXSTD_BARRIER_H +#define STLXSTD_BARRIER_H + +#include +#include + +namespace stlxstd { + +class barrier { +public: + explicit barrier(uint32_t count) { + stlx_barrier_init(&b_, count); + } + + ~barrier() = default; + + barrier(const barrier&) = delete; + barrier& operator=(const barrier&) = delete; + + void arrive_and_wait() { stlx_barrier_wait(&b_); } + +private: + stlx_barrier_t b_; +}; + +} // namespace stlxstd + +#endif // STLXSTD_BARRIER_H diff --git a/userland/lib/libstlxcxx/include/stlxstd/condition_variable.h b/userland/lib/libstlxcxx/include/stlxstd/condition_variable.h new file mode 100644 index 00000000..f6a1308c --- /dev/null +++ b/userland/lib/libstlxcxx/include/stlxstd/condition_variable.h @@ -0,0 +1,35 @@ +#ifndef STLXSTD_CONDITION_VARIABLE_H +#define STLXSTD_CONDITION_VARIABLE_H + +#include +#include + +namespace stlxstd { + +class condition_variable { +public: + condition_variable() : cv_(STLX_COND_INIT) {} + ~condition_variable() = default; + + condition_variable(const condition_variable&) = delete; + condition_variable& operator=(const condition_variable&) = delete; + + void wait(unique_lock& lock) { + stlx_cond_wait(&cv_, lock.mutex_ptr()->native()); + } + + template + void wait(unique_lock& lock, Pred pred) { + while (!pred()) wait(lock); + } + + void notify_one() { stlx_cond_signal(&cv_); } + void notify_all() { stlx_cond_broadcast(&cv_); } + +private: + stlx_cond_t cv_; +}; + +} // namespace stlxstd + +#endif // STLXSTD_CONDITION_VARIABLE_H diff --git a/userland/lib/libstlxcxx/include/stlxstd/mutex.h b/userland/lib/libstlxcxx/include/stlxstd/mutex.h new file mode 100644 index 00000000..940be96b --- /dev/null +++ b/userland/lib/libstlxcxx/include/stlxstd/mutex.h @@ -0,0 +1,82 @@ +#ifndef STLXSTD_MUTEX_H +#define STLXSTD_MUTEX_H + +#include + +namespace stlxstd { + +struct defer_lock_t {}; +inline constexpr defer_lock_t defer_lock{}; + +class mutex { +public: + mutex() : m_(STLX_MUTEX_INIT) {} + ~mutex() = default; + + mutex(const mutex&) = delete; + mutex& operator=(const mutex&) = delete; + + void lock() { stlx_mutex_lock(&m_); } + void unlock() { stlx_mutex_unlock(&m_); } + bool try_lock() { return stlx_mutex_trylock(&m_) == 0; } + + stlx_mutex_t* native() { return &m_; } + +private: + stlx_mutex_t m_; +}; + +template +class lock_guard { +public: + explicit lock_guard(Mutex& m) : m_(m) { m_.lock(); } + ~lock_guard() { m_.unlock(); } + + lock_guard(const lock_guard&) = delete; + lock_guard& operator=(const lock_guard&) = delete; + +private: + Mutex& m_; +}; + +template +class unique_lock { +public: + explicit unique_lock(Mutex& m) : m_(&m), owned_(true) { m_->lock(); } + unique_lock(Mutex& m, defer_lock_t) : m_(&m), owned_(false) {} + ~unique_lock() { if (owned_) m_->unlock(); } + + unique_lock(const unique_lock&) = delete; + unique_lock& operator=(const unique_lock&) = delete; + + unique_lock(unique_lock&& o) : m_(o.m_), owned_(o.owned_) { + o.m_ = nullptr; + o.owned_ = false; + } + + unique_lock& operator=(unique_lock&& o) { + if (this != &o) { + if (owned_) m_->unlock(); + m_ = o.m_; + owned_ = o.owned_; + o.m_ = nullptr; + o.owned_ = false; + } + return *this; + } + + void lock() { m_->lock(); owned_ = true; } + void unlock() { m_->unlock(); owned_ = false; } + bool try_lock() { owned_ = m_->try_lock(); return owned_; } + + bool owns_lock() const { return owned_; } + Mutex* mutex_ptr() const { return m_; } + +private: + Mutex* m_; + bool owned_; +}; + +} // namespace stlxstd + +#endif // STLXSTD_MUTEX_H diff --git a/userland/lib/libstlxcxx/include/stlxstd/thread.h b/userland/lib/libstlxcxx/include/stlxstd/thread.h new file mode 100644 index 00000000..57ca39c5 --- /dev/null +++ b/userland/lib/libstlxcxx/include/stlxstd/thread.h @@ -0,0 +1,117 @@ +#ifndef STLXSTD_THREAD_H +#define STLXSTD_THREAD_H + +#include +#include +#include +#include + +namespace stlxstd { +namespace detail { + +struct thread_context { + void (*invoke)(thread_context*); +}; + +template +struct thread_context_impl : thread_context { + Fn fn; + + explicit thread_context_impl(Fn&& f) : fn(static_cast(f)) { + invoke = [](thread_context* base) { + auto* self = static_cast(base); + self->fn(); + }; + } +}; + +// Defined in thread.cpp +extern "C" void stlxstd_thread_entry(void* arg); + +} // namespace detail + +class thread { +public: + static constexpr size_t STACK_SIZE = 64 * 1024; + + thread() : m_handle(-1), m_stack(nullptr) {} + + template + explicit thread(Fn&& fn) { + using ctx_t = detail::thread_context_impl; + auto* ctx = static_cast(malloc(sizeof(ctx_t))); + if (!ctx) abort(); + new (ctx) ctx_t(static_cast(fn)); + + m_stack = mmap(nullptr, STACK_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); + if (m_stack == MAP_FAILED) { + ctx->~ctx_t(); + free(ctx); + abort(); + } + + void* stack_top = static_cast(m_stack) + STACK_SIZE; + m_handle = proc_create_thread(detail::stlxstd_thread_entry, ctx, + stack_top, "stlxstd"); + if (m_handle < 0) { + ctx->~ctx_t(); + free(ctx); + munmap(m_stack, STACK_SIZE); + m_stack = nullptr; + abort(); + } + + proc_thread_start(m_handle); + } + + ~thread() { + if (joinable()) abort(); + } + + thread(const thread&) = delete; + thread& operator=(const thread&) = delete; + + thread(thread&& o) : m_handle(o.m_handle), m_stack(o.m_stack) { + o.m_handle = -1; + o.m_stack = nullptr; + } + + thread& operator=(thread&& o) { + if (this != &o) { + if (joinable()) abort(); + m_handle = o.m_handle; + m_stack = o.m_stack; + o.m_handle = -1; + o.m_stack = nullptr; + } + return *this; + } + + bool joinable() const { return m_handle >= 0; } + + void join() { + if (!joinable()) return; + proc_thread_join(m_handle, nullptr); + munmap(m_stack, STACK_SIZE); + m_handle = -1; + m_stack = nullptr; + } + + // Stack is intentionally not freed on detach. The thread is still + // using it. It will be reclaimed when the process exits. + void detach() { + if (!joinable()) return; + proc_thread_detach(m_handle); + m_handle = -1; + m_stack = nullptr; + } + +private: + int m_handle; + void* m_stack; +}; + +} // namespace stlxstd + +#endif // STLXSTD_THREAD_H diff --git a/userland/lib/libstlxcxx/src/thread.cpp b/userland/lib/libstlxcxx/src/thread.cpp new file mode 100644 index 00000000..c0deeb06 --- /dev/null +++ b/userland/lib/libstlxcxx/src/thread.cpp @@ -0,0 +1,14 @@ +#include +#include +#include + +namespace stlxstd::detail { + +extern "C" void stlxstd_thread_entry(void* arg) { + auto* ctx = static_cast(arg); + ctx->invoke(ctx); + free(ctx); + _exit(0); +} + +} // namespace stlxstd::detail From 49864fc1035302682ef942baa355b5724d342187 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 21:54:01 +0000 Subject: [PATCH 3/6] feat(userland): add synctest app and fix extern C linkage in libstlx headers synctest validates stlxstd:: primitives: mutex stress (8 threads), condvar producer/consumer, barrier sync, thread join/detach, and multi-mutex independence. Added extern "C" guards to all libstlx C headers (futex.h, mutex.h, cond.h, barrier.h, proc.h) so C++ code links correctly against the C implementations. Co-authored-by: Albert Slepak --- userland/apps/Makefile | 2 +- userland/apps/synctest/Makefile | 3 + userland/apps/synctest/src/synctest.cpp | 209 ++++++++++++++++++ userland/lib/libstlx/include/stlx/barrier.h | 8 + userland/lib/libstlx/include/stlx/cond.h | 8 + userland/lib/libstlx/include/stlx/futex.h | 8 + userland/lib/libstlx/include/stlx/mutex.h | 8 + userland/lib/libstlx/include/stlx/proc.h | 8 + .../lib/libstlxcxx/include/stlxstd/thread.h | 1 + 9 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 userland/apps/synctest/Makefile create mode 100644 userland/apps/synctest/src/synctest.cpp diff --git a/userland/apps/Makefile b/userland/apps/Makefile index 1c5551b1..e60102db 100644 --- a/userland/apps/Makefile +++ b/userland/apps/Makefile @@ -5,7 +5,7 @@ APP_DIRS := init hello shell ls cat rm stat touch sleep true false clear ptytest date \ clockbench stlxdm stlxterm doom ping ifconfig nslookup arp udpecho tcpecho \ fetch polltest dropbear blackjack wordle hangman snake tetris \ - grep wc head threadtest uname kill cxxtest + grep wc head threadtest uname kill cxxtest synctest APP_COUNT := $(words $(APP_DIRS)) all: diff --git a/userland/apps/synctest/Makefile b/userland/apps/synctest/Makefile new file mode 100644 index 00000000..f0e3775d --- /dev/null +++ b/userland/apps/synctest/Makefile @@ -0,0 +1,3 @@ +APP_NAME := synctest +APP_LIBS := stlxcxx +include ../../mk/cxxapp.mk diff --git a/userland/apps/synctest/src/synctest.cpp b/userland/apps/synctest/src/synctest.cpp new file mode 100644 index 00000000..ad52af5a --- /dev/null +++ b/userland/apps/synctest/src/synctest.cpp @@ -0,0 +1,209 @@ +#include +#include +#include +#include +#include +#include +#include + +static int g_passed = 0; +static int g_failed = 0; + +static void check(const char* name, bool ok) { + printf(" %s: %s\n", ok ? "PASS" : "FAIL", name); + if (ok) g_passed++; + else g_failed++; +} + +// --- Test 1: Mutex stress --- + +static constexpr int MUTEX_THREADS = 8; +static constexpr int MUTEX_ITERS = 10000; + +static void test_mutex_stress() { + stlxstd::mutex mtx; + int counter = 0; + + stlxstd::thread threads[MUTEX_THREADS]; + for (int i = 0; i < MUTEX_THREADS; i++) { + threads[i] = stlxstd::thread([&] { + for (int j = 0; j < MUTEX_ITERS; j++) { + stlxstd::lock_guard guard(mtx); + counter++; + } + }); + } + for (int i = 0; i < MUTEX_THREADS; i++) { + threads[i].join(); + } + + check("mutex stress (8 threads x 10000)", counter == MUTEX_THREADS * MUTEX_ITERS); +} + +// --- Test 2: Condition variable producer/consumer --- + +static constexpr int ITEMS = 100; + +static void test_condvar_producer_consumer() { + stlxstd::mutex mtx; + stlxstd::condition_variable cv; + int produced = 0; + int consumed = 0; + bool done = false; + + stlxstd::thread consumer([&] { + stlxstd::unique_lock lock(mtx); + while (!done || produced > consumed) { + cv.wait(lock, [&] { return produced > consumed || done; }); + while (produced > consumed) { + consumed++; + } + } + }); + + stlxstd::thread producer([&] { + for (int i = 0; i < ITEMS; i++) { + { + stlxstd::lock_guard guard(mtx); + produced++; + } + cv.notify_one(); + } + { + stlxstd::lock_guard guard(mtx); + done = true; + } + cv.notify_one(); + }); + + producer.join(); + consumer.join(); + + check("condvar producer/consumer (100 items)", consumed == ITEMS); +} + +// --- Test 3: Barrier synchronization --- + +static constexpr int BARRIER_THREADS = 4; + +static void test_barrier() { + stlxstd::barrier bar(BARRIER_THREADS); + volatile int flags[BARRIER_THREADS] = {}; + volatile int verified[BARRIER_THREADS] = {}; + + stlxstd::thread threads[BARRIER_THREADS]; + for (int i = 0; i < BARRIER_THREADS; i++) { + threads[i] = stlxstd::thread([&, i] { + __atomic_store_n(&flags[i], 1, __ATOMIC_RELEASE); + bar.arrive_and_wait(); + // After barrier: all flags must be set + bool all_set = true; + for (int j = 0; j < BARRIER_THREADS; j++) { + if (!__atomic_load_n(&flags[j], __ATOMIC_ACQUIRE)) { + all_set = false; + } + } + __atomic_store_n(&verified[i], all_set ? 1 : 0, __ATOMIC_RELEASE); + }); + } + for (int i = 0; i < BARRIER_THREADS; i++) { + threads[i].join(); + } + + bool ok = true; + for (int i = 0; i < BARRIER_THREADS; i++) { + if (!verified[i]) ok = false; + } + check("barrier (4 threads)", ok); +} + +// --- Test 4: Thread join --- + +static void test_thread_join() { + volatile int value = 0; + + stlxstd::thread t([&] { + __atomic_store_n(&value, 42, __ATOMIC_RELEASE); + }); + t.join(); + + check("thread join", __atomic_load_n(&value, __ATOMIC_ACQUIRE) == 42); +} + +// --- Test 5: Thread detach --- + +static void test_thread_detach() { + volatile int flag = 0; + + { + stlxstd::thread t([&] { + __atomic_store_n(&flag, 1, __ATOMIC_RELEASE); + }); + t.detach(); + } + + // Brief busy-wait for the detached thread to run + for (int i = 0; i < 10000000; i++) { + if (__atomic_load_n(&flag, __ATOMIC_ACQUIRE)) break; + asm volatile("" ::: "memory"); + } + + check("thread detach", __atomic_load_n(&flag, __ATOMIC_ACQUIRE) == 1); +} + +// --- Test 6: Multiple independent mutexes --- + +static constexpr int MULTI_THREADS = 4; +static constexpr int MULTI_ITERS = 5000; + +static void test_multi_mutex() { + stlxstd::mutex mtx_a, mtx_b; + int counter_a = 0; + int counter_b = 0; + + stlxstd::thread threads[MULTI_THREADS]; + for (int i = 0; i < MULTI_THREADS; i++) { + threads[i] = stlxstd::thread([&] { + for (int j = 0; j < MULTI_ITERS; j++) { + { + stlxstd::lock_guard guard(mtx_a); + counter_a++; + } + { + stlxstd::lock_guard guard(mtx_b); + counter_b++; + } + } + }); + } + for (int i = 0; i < MULTI_THREADS; i++) { + threads[i].join(); + } + + bool ok = (counter_a == MULTI_THREADS * MULTI_ITERS) && + (counter_b == MULTI_THREADS * MULTI_ITERS); + check("multi-mutex (2 locks, 4 threads x 5000)", ok); +} + +int main() { + printf("\nsynctest: Stellux synchronization test suite\n\n"); + + printf("[mutex]\n"); + test_mutex_stress(); + + printf("\n[condition variable]\n"); + test_condvar_producer_consumer(); + + printf("\n[barrier]\n"); + test_barrier(); + + printf("\n[thread lifecycle]\n"); + test_thread_join(); + test_thread_detach(); + + printf("\n[multi-mutex]\n"); + test_multi_mutex(); + + printf("\n--- Results: %d passed, %d failed ---\n\n", g_passed, g_failed); + return g_failed > 0 ? 1 : 0; +} diff --git a/userland/lib/libstlx/include/stlx/barrier.h b/userland/lib/libstlx/include/stlx/barrier.h index 082fab7f..234617e5 100644 --- a/userland/lib/libstlx/include/stlx/barrier.h +++ b/userland/lib/libstlx/include/stlx/barrier.h @@ -3,6 +3,10 @@ #include +#ifdef __cplusplus +extern "C" { +#endif + typedef struct { uint32_t count; uint32_t generation; @@ -14,4 +18,8 @@ void stlx_barrier_init(stlx_barrier_t* b, uint32_t count); /* Block until all count threads have called barrier_wait. Reusable. */ void stlx_barrier_wait(stlx_barrier_t* b); +#ifdef __cplusplus +} +#endif + #endif /* STLX_BARRIER_H */ diff --git a/userland/lib/libstlx/include/stlx/cond.h b/userland/lib/libstlx/include/stlx/cond.h index f5740000..5c27b8e2 100644 --- a/userland/lib/libstlx/include/stlx/cond.h +++ b/userland/lib/libstlx/include/stlx/cond.h @@ -4,6 +4,10 @@ #include #include +#ifdef __cplusplus +extern "C" { +#endif + typedef struct { uint32_t seq; } stlx_cond_t; #define STLX_COND_INIT { 0 } @@ -18,4 +22,8 @@ void stlx_cond_signal(stlx_cond_t* cv); /* Wake all waiters. */ void stlx_cond_broadcast(stlx_cond_t* cv); +#ifdef __cplusplus +} +#endif + #endif /* STLX_COND_H */ diff --git a/userland/lib/libstlx/include/stlx/futex.h b/userland/lib/libstlx/include/stlx/futex.h index bb124e8f..39e31966 100644 --- a/userland/lib/libstlx/include/stlx/futex.h +++ b/userland/lib/libstlx/include/stlx/futex.h @@ -3,6 +3,10 @@ #include +#ifdef __cplusplus +extern "C" { +#endif + /* Block if *addr == expected. timeout_ns=0 waits indefinitely. * Returns 0 on wake, negative errno on error (-EAGAIN, -ETIMEDOUT). */ int stlx_futex_wait(uint32_t* addr, uint32_t expected, uint64_t timeout_ns); @@ -13,4 +17,8 @@ int stlx_futex_wake(uint32_t* addr, uint32_t count); /* Wake all threads waiting on addr. Returns number woken. */ int stlx_futex_wake_all(uint32_t* addr); +#ifdef __cplusplus +} +#endif + #endif /* STLX_FUTEX_H */ diff --git a/userland/lib/libstlx/include/stlx/mutex.h b/userland/lib/libstlx/include/stlx/mutex.h index b5e87f4a..36ef9e27 100644 --- a/userland/lib/libstlx/include/stlx/mutex.h +++ b/userland/lib/libstlx/include/stlx/mutex.h @@ -3,6 +3,10 @@ #include +#ifdef __cplusplus +extern "C" { +#endif + /* State 0: unlocked, 1: locked (no waiters), 2: locked (with waiters) */ typedef struct { uint32_t state; } stlx_mutex_t; @@ -14,4 +18,8 @@ void stlx_mutex_unlock(stlx_mutex_t* m); /* Returns 0 if acquired, -1 if already held. */ int stlx_mutex_trylock(stlx_mutex_t* m); +#ifdef __cplusplus +} +#endif + #endif /* STLX_MUTEX_H */ diff --git a/userland/lib/libstlx/include/stlx/proc.h b/userland/lib/libstlx/include/stlx/proc.h index 13c1541b..84e96684 100644 --- a/userland/lib/libstlx/include/stlx/proc.h +++ b/userland/lib/libstlx/include/stlx/proc.h @@ -3,6 +3,10 @@ #include +#ifdef __cplusplus +extern "C" { +#endif + /* Wait status decode macros (Linux-compatible bit layout). */ #define STLX_WIFEXITED(s) (((s) & 0x7F) == 0) #define STLX_WEXITSTATUS(s) (((s) >> 8) & 0xFF) @@ -96,4 +100,8 @@ static inline int proc_thread_join(int handle, int* exit_code) { return proc_wai static inline int proc_thread_detach(int handle) { return proc_detach(handle); } static inline int proc_thread_kill(int handle) { return proc_kill(handle); } +#ifdef __cplusplus +} +#endif + #endif /* STLX_PROC_H */ diff --git a/userland/lib/libstlxcxx/include/stlxstd/thread.h b/userland/lib/libstlxcxx/include/stlxstd/thread.h index 57ca39c5..39c23da2 100644 --- a/userland/lib/libstlxcxx/include/stlxstd/thread.h +++ b/userland/lib/libstlxcxx/include/stlxstd/thread.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace stlxstd { namespace detail { From 311106f1f0fbeec5c853e40ec7f557cb9e8666f4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 22:11:55 +0000 Subject: [PATCH 4/6] fix(sched): inherit creator's TLS base in user threads New user threads had tls_base=0, causing a null dereference on any FS-relative access (musl's errno, malloc internals, etc.). Since threads share the creator's address space, inheriting the TLS base gives them a valid FS segment for musl's thread-local storage. Co-authored-by: Albert Slepak --- kernel/sched/sched.cpp | 2 +- userland/lib/libstlxcxx/include/stlxstd/thread.h | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/kernel/sched/sched.cpp b/kernel/sched/sched.cpp index e4dbefd5..31949340 100644 --- a/kernel/sched/sched.cpp +++ b/kernel/sched/sched.cpp @@ -816,7 +816,7 @@ __PRIVILEGED_CODE task* create_user_thread( t->exec.system_stack_top = sys_stack_top; t->exec.pt_root = paging::supervisor_pt_root_for_user_task(creator->exec.mm_ctx->pt_root); t->exec.user_pt_root = creator->exec.mm_ctx->pt_root; - t->exec.tls_base = 0; + t->exec.tls_base = creator->exec.tls_base; t->task_stack_base = 0; // user stack is not VMM-allocated t->sys_stack_base = sys_stack_base; diff --git a/userland/lib/libstlxcxx/include/stlxstd/thread.h b/userland/lib/libstlxcxx/include/stlxstd/thread.h index 39c23da2..1c798c8e 100644 --- a/userland/lib/libstlxcxx/include/stlxstd/thread.h +++ b/userland/lib/libstlxcxx/include/stlxstd/thread.h @@ -18,11 +18,13 @@ template struct thread_context_impl : thread_context { Fn fn; + static void call(thread_context* base) { + auto* self = static_cast(base); + self->fn(); + } + explicit thread_context_impl(Fn&& f) : fn(static_cast(f)) { - invoke = [](thread_context* base) { - auto* self = static_cast(base); - self->fn(); - }; + invoke = &call; } }; From c103a30f98fa53c3356e934de175e26a4c735901 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 22:38:15 +0000 Subject: [PATCH 5/6] fix(userland): address bugbot findings in barrier and thread 1. Barrier: eliminated split-update race between count reset and generation advance. Count now grows monotonically; last thread per round is identified by count % total == 0. No reset needed. 2. Thread: added destroy function pointer to thread_context so the callable's destructor runs before free(). Prevents resource leaks when lambdas capture RAII types by value. Co-authored-by: Albert Slepak --- userland/lib/libstlx/src/barrier.c | 7 +++++-- userland/lib/libstlxcxx/include/stlxstd/thread.h | 6 ++++++ userland/lib/libstlxcxx/src/thread.cpp | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/userland/lib/libstlx/src/barrier.c b/userland/lib/libstlx/src/barrier.c index 89d31d80..71893a8a 100644 --- a/userland/lib/libstlx/src/barrier.c +++ b/userland/lib/libstlx/src/barrier.c @@ -9,10 +9,13 @@ void stlx_barrier_init(stlx_barrier_t* b, uint32_t count) { void stlx_barrier_wait(stlx_barrier_t* b) { uint32_t gen = __atomic_load_n(&b->generation, __ATOMIC_ACQUIRE); + + // Count grows monotonically (no reset). The last thread in each round + // is identified by count being a multiple of total. This eliminates + // the race between resetting count and advancing generation. uint32_t arrived = __atomic_fetch_add(&b->count, 1, __ATOMIC_ACQ_REL) + 1; - if (arrived == b->total) { - __atomic_store_n(&b->count, 0, __ATOMIC_RELAXED); + if (arrived % b->total == 0) { __atomic_fetch_add(&b->generation, 1, __ATOMIC_RELEASE); stlx_futex_wake_all(&b->generation); } else { diff --git a/userland/lib/libstlxcxx/include/stlxstd/thread.h b/userland/lib/libstlxcxx/include/stlxstd/thread.h index 1c798c8e..5574bdcc 100644 --- a/userland/lib/libstlxcxx/include/stlxstd/thread.h +++ b/userland/lib/libstlxcxx/include/stlxstd/thread.h @@ -12,6 +12,7 @@ namespace detail { struct thread_context { void (*invoke)(thread_context*); + void (*destroy)(thread_context*); }; template @@ -23,8 +24,13 @@ struct thread_context_impl : thread_context { self->fn(); } + static void destruct(thread_context* base) { + static_cast(base)->~thread_context_impl(); + } + explicit thread_context_impl(Fn&& f) : fn(static_cast(f)) { invoke = &call; + destroy = &destruct; } }; diff --git a/userland/lib/libstlxcxx/src/thread.cpp b/userland/lib/libstlxcxx/src/thread.cpp index c0deeb06..4a967eea 100644 --- a/userland/lib/libstlxcxx/src/thread.cpp +++ b/userland/lib/libstlxcxx/src/thread.cpp @@ -7,6 +7,7 @@ namespace stlxstd::detail { extern "C" void stlxstd_thread_entry(void* arg) { auto* ctx = static_cast(arg); ctx->invoke(ctx); + ctx->destroy(ctx); free(ctx); _exit(0); } From 9eab0a6f3aa0534450715888567c5eac1affa257 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 23:01:35 +0000 Subject: [PATCH 6/6] fix(userland): decay thread callable type and widen barrier count 1. Thread: decay Fn so passing an lvalue callable stores a copy, not a dangling reference. Matches std::thread behavior. 2. Barrier: use uint64_t for monotonic count to prevent uint32 wrap from causing premature barrier release. Co-authored-by: Albert Slepak --- userland/lib/libstlx/include/stlx/barrier.h | 2 +- userland/lib/libstlx/src/barrier.c | 2 +- userland/lib/libstlxcxx/include/stlxstd/thread.h | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/userland/lib/libstlx/include/stlx/barrier.h b/userland/lib/libstlx/include/stlx/barrier.h index 234617e5..2b6cabb8 100644 --- a/userland/lib/libstlx/include/stlx/barrier.h +++ b/userland/lib/libstlx/include/stlx/barrier.h @@ -8,7 +8,7 @@ extern "C" { #endif typedef struct { - uint32_t count; + uint64_t count; uint32_t generation; uint32_t total; } stlx_barrier_t; diff --git a/userland/lib/libstlx/src/barrier.c b/userland/lib/libstlx/src/barrier.c index 71893a8a..b474e759 100644 --- a/userland/lib/libstlx/src/barrier.c +++ b/userland/lib/libstlx/src/barrier.c @@ -13,7 +13,7 @@ void stlx_barrier_wait(stlx_barrier_t* b) { // Count grows monotonically (no reset). The last thread in each round // is identified by count being a multiple of total. This eliminates // the race between resetting count and advancing generation. - uint32_t arrived = __atomic_fetch_add(&b->count, 1, __ATOMIC_ACQ_REL) + 1; + uint64_t arrived = __atomic_fetch_add(&b->count, 1, __ATOMIC_ACQ_REL) + 1; if (arrived % b->total == 0) { __atomic_fetch_add(&b->generation, 1, __ATOMIC_RELEASE); diff --git a/userland/lib/libstlxcxx/include/stlxstd/thread.h b/userland/lib/libstlxcxx/include/stlxstd/thread.h index 5574bdcc..ca07943b 100644 --- a/userland/lib/libstlxcxx/include/stlxstd/thread.h +++ b/userland/lib/libstlxcxx/include/stlxstd/thread.h @@ -8,6 +8,11 @@ #include namespace stlxstd { + +template struct remove_ref { using type = T; }; +template struct remove_ref { using type = T; }; +template struct remove_ref { using type = T; }; + namespace detail { struct thread_context { @@ -47,7 +52,10 @@ class thread { template explicit thread(Fn&& fn) { - using ctx_t = detail::thread_context_impl; + // Decay Fn so lvalue references become value types (same as std::thread). + // Without this, passing an lvalue stores a reference that can dangle. + using Decayed = typename remove_ref::type; + using ctx_t = detail::thread_context_impl; auto* ctx = static_cast(malloc(sizeof(ctx_t))); if (!ctx) abort(); new (ctx) ctx_t(static_cast(fn));