From 89b31dba40a0c157e0421355c86213a5caef6e77 Mon Sep 17 00:00:00 2001 From: kevodwyer Date: Thu, 21 May 2026 12:40:32 +0100 Subject: [PATCH 1/3] docs: add NTSYNC implementation plan --- docs/NTSYNC-Implementation-Plan.md | 205 +++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 docs/NTSYNC-Implementation-Plan.md diff --git a/docs/NTSYNC-Implementation-Plan.md b/docs/NTSYNC-Implementation-Plan.md new file mode 100644 index 000000000..7acc68d03 --- /dev/null +++ b/docs/NTSYNC-Implementation-Plan.md @@ -0,0 +1,205 @@ +# NTSYNC Implementation Plan + +This document tracks the plan for making Boxedwine compatible with Wine's NTSYNC path. NTSYNC is exposed to Linux userspace as a character device, `/dev/ntsync`, controlled through `ioctl()` calls. Boxedwine already has virtual device nodes and ioctl dispatch, so the work should fit into the existing syscall and filesystem model. + +References: + +- Linux kernel NTSYNC userspace API: https://docs.kernel.org/userspace-api/ntsync.html +- Linux NTSYNC uAPI header: https://raw.githubusercontent.com/torvalds/linux/master/include/uapi/linux/ntsync.h + +## Goals + +- [ ] Let Wine detect `/dev/ntsync` inside Boxedwine. +- [ ] Implement the NTSYNC ioctl ABI closely enough for Wine 11 and newer to use it. +- [ ] Use host Linux `/dev/ntsync` when available for real kernel-level synchronization benefits. +- [ ] Provide a portable emulated backend for non-Linux hosts and for systems without host NTSYNC. +- [ ] Add focused tests for NTSYNC object semantics and Wine-facing behavior. + +## Non-Goals + +- Do not add a new Linux syscall for NTSYNC. Wine uses `/dev/ntsync` plus `ioctl()`. +- Do not expose NTSYNC as a general Boxedwine synchronization API outside of the Linux compatibility layer. +- Do not require host NTSYNC for basic compatibility; native acceleration should be optional. + +## Current Boxedwine Touchpoints + +- Virtual `/dev` nodes are registered in `source/sdl/startupArgs.cpp`. +- Device implementations live under `source/kernel/devs/`. +- Device headers live under `include/`. +- `ioctl()` dispatch flows through `source/kernel/syscall.cpp`, `KProcess::ioctl()`, `KFile::ioctl()`, and then `FsOpenNode::ioctl()`. +- Returned NTSYNC objects should be represented as guest file descriptors using `KProcess::allocFileDescriptor()`. +- Existing condition and wait helpers live in `source/util/synchronization.*`. +- Existing futex behavior lives in `source/kernel/kthread.cpp` and can inform fallback wait behavior. + +## Proposed Architecture + +### Device and Context + +- [ ] Add `include/devntsync.h`. +- [ ] Add `source/kernel/devs/devntsync.cpp`. +- [ ] Register `/dev/ntsync` in `StartUpArgs::buildVirtualFileSystem()`. +- [ ] Opening `/dev/ntsync` creates one `NTSyncContext`. +- [ ] Objects created from one context must not be waitable with objects from another context. + +### Object Model + +- [ ] Add an NTSYNC object representation for: + - [ ] semaphore + - [ ] mutex + - [ ] event +- [ ] Represent created objects as normal guest file descriptors. +- [ ] Ensure object lifetime follows FD lifetime. +- [ ] Implement useful `selfFd()` labels for `/proc/self/fd`. + +### Backend Model + +- [ ] Add a backend boundary so native and emulated implementations share the ioctl-facing ABI. +- [ ] Native Linux backend: + - [ ] Open host `/dev/ntsync`. + - [ ] Create host NTSYNC objects and store host FDs in guest object wrappers. + - [ ] Translate guest FD arrays for wait ioctls into host FD arrays. + - [ ] Copy output fields back to guest memory. +- [ ] Emulated backend: + - [ ] Use a context-level lock and condition variable. + - [ ] Implement atomic wait-any and wait-all semantics. + - [ ] Wake waiters after state-changing operations. + +## ioctl Surface + +Implement the ioctl constants and structures from `linux/ntsync.h`. + +- [ ] `NTSYNC_IOC_CREATE_SEM` +- [ ] `NTSYNC_IOC_CREATE_MUTEX` +- [ ] `NTSYNC_IOC_CREATE_EVENT` +- [ ] `NTSYNC_IOC_SEM_RELEASE` +- [ ] `NTSYNC_IOC_SEM_READ` +- [ ] `NTSYNC_IOC_MUTEX_UNLOCK` +- [ ] `NTSYNC_IOC_MUTEX_KILL` +- [ ] `NTSYNC_IOC_MUTEX_READ` +- [ ] `NTSYNC_IOC_EVENT_SET` +- [ ] `NTSYNC_IOC_EVENT_RESET` +- [ ] `NTSYNC_IOC_EVENT_PULSE` +- [ ] `NTSYNC_IOC_EVENT_READ` +- [ ] `NTSYNC_IOC_WAIT_ANY` +- [ ] `NTSYNC_IOC_WAIT_ALL` + +## Semantic Requirements + +- [ ] Semaphore count must never exceed max; overflow returns `EOVERFLOW`. +- [ ] Acquiring a semaphore decrements its count. +- [ ] Mutex owner zero means unowned. +- [ ] Mutex recursion count and owner must be consistent at create time. +- [ ] Unlock by owner zero returns `EINVAL`. +- [ ] Unlock by non-owner returns `EPERM`. +- [ ] Killed mutexes become abandoned and unowned. +- [ ] Acquiring an abandoned mutex returns `EOWNERDEAD` while still acquiring it. +- [ ] Auto-reset events become unsignaled when acquired. +- [ ] Manual-reset events remain signaled when acquired. +- [ ] Pulse wakes eligible waiters and leaves the event unsignaled. +- [ ] `WAIT_ANY` acquires at most one object and writes the selected index. +- [ ] `WAIT_ALL` acquires all objects atomically or modifies none. +- [ ] `WAIT_ALL` rejects duplicate objects as the kernel API specifies. +- [ ] Timeouts use absolute nanoseconds and honor `NTSYNC_WAIT_REALTIME`. +- [ ] Signals and thread termination return appropriate interrupt behavior. + +## Implementation Phases + +### Phase 1: Device Discovery + +- [ ] Add `/dev/ntsync` registration. +- [ ] Add placeholder device implementation. +- [ ] Add ioctl constant definitions. +- [ ] Make unsupported ioctls return Linux-compatible errors. +- [ ] Verify Wine opens `/dev/ntsync`. + +Acceptance: + +- [ ] A small guest program can `stat()` and `open()` `/dev/ntsync`. +- [ ] Wine 11 attempts NTSYNC ioctls instead of immediately falling back. + +### Phase 2: Object Creation + +- [ ] Implement create semaphore. +- [ ] Implement create mutex. +- [ ] Implement create event. +- [ ] Return valid guest FDs for created objects. +- [ ] Reject invalid create arguments. + +Acceptance: + +- [ ] Guest test can create each object type and close returned FDs. +- [ ] Invalid create inputs return expected errors. + +### Phase 3: Read and State Mutation + +- [ ] Implement semaphore release/read. +- [ ] Implement mutex unlock/kill/read. +- [ ] Implement event set/reset/pulse/read. +- [ ] Wake waiters when object state changes. + +Acceptance: + +- [ ] Unit tests cover state transitions for every object type. +- [ ] Error returns match Linux NTSYNC behavior. + +### Phase 4: Wait Semantics + +- [ ] Implement `WAIT_ANY`. +- [ ] Implement `WAIT_ALL`. +- [ ] Implement alert event handling. +- [ ] Implement timeout handling. +- [ ] Implement abandoned mutex results during waits. + +Acceptance: + +- [ ] Tests verify single-object waits. +- [ ] Tests verify multi-object wait-any index selection. +- [ ] Tests verify wait-all atomicity. +- [ ] Tests verify alert event behavior. +- [ ] Tests verify timeout behavior. + +### Phase 5: Native Linux Acceleration + +- [ ] Detect host `/dev/ntsync` availability. +- [ ] Forward create and mutation ioctls to host backend. +- [ ] Translate guest wait FD arrays to host wait FD arrays. +- [ ] Fall back to emulated backend if host support is unavailable or disabled. + +Acceptance: + +- [ ] On Linux with NTSYNC, Boxedwine uses host NTSYNC. +- [ ] On hosts without NTSYNC, Wine still sees a working emulated device. +- [ ] Native and emulated backends pass the same behavioral tests. + +### Phase 6: Wine Validation and Benchmarking + +- [ ] Build or install a Wine version with NTSYNC support. +- [ ] Add logging around `/dev/ntsync` open and ioctl paths. +- [ ] Confirm Wine uses NTSYNC under Boxedwine. +- [ ] Run at least one sync-heavy app or game workload. +- [ ] Compare behavior and performance against the pre-NTSYNC path. + +Acceptance: + +- [ ] Wine does not fall back because of missing or broken NTSYNC. +- [ ] No regressions in existing Wine startup or common app workflows. +- [ ] Performance results are recorded in `docs/Performance.md` or a linked benchmark note. + +## Build and Project Updates + +- [ ] Linux makefile should pick up new `source/**/*.cpp` files automatically. +- [ ] Update Visual Studio project files for new `.cpp` and `.h` files. +- [ ] Check macOS/Xcode project handling if NTSYNC files are not auto-discovered there. +- [ ] Ensure native Linux backend code is guarded so non-Linux builds compile cleanly. + +## Open Questions + +- [ ] Should native NTSYNC be enabled automatically, or controlled by a startup flag? +- [ ] What major/minor device number should Boxedwine expose for virtual `/dev/ntsync`? +- [ ] Should emulated NTSYNC be advertised by default on every host, or only when Wine probes for it? +- [ ] How should diagnostic logging expose native versus emulated backend selection? +- [ ] Which Wine build should be used as the compatibility baseline? + +## Progress Log + +- 2026-05-21: Initial plan recorded after reviewing Boxedwine syscall/device structure and Linux NTSYNC uAPI. From 6200029769d722da8dcf77160eefd2d31710e77c Mon Sep 17 00:00:00 2001 From: kevodwyer Date: Thu, 21 May 2026 13:38:40 +0100 Subject: [PATCH 2/3] wip: start NTSYNC device implementation --- include/devntsync.h | 15 ++ include/kerror.h | 14 +- include/kobject.h | 13 +- source/kernel/devs/devntsync.cpp | 348 +++++++++++++++++++++++++++++++ source/sdl/startupArgs.cpp | 16 +- 5 files changed, 387 insertions(+), 19 deletions(-) create mode 100644 include/devntsync.h create mode 100644 source/kernel/devs/devntsync.cpp diff --git a/include/devntsync.h b/include/devntsync.h new file mode 100644 index 000000000..0d38540c0 --- /dev/null +++ b/include/devntsync.h @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2012-2026 The BoxedWine Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef __DEVNTSYNC_H__ +#define __DEVNTSYNC_H__ + +FsOpenNode* openDevNTSync(const std::shared_ptr& node, U32 flags, U32 data); + +#endif diff --git a/include/kerror.h b/include/kerror.h index 2b3dd6365..863602478 100644 --- a/include/kerror.h +++ b/include/kerror.h @@ -56,8 +56,9 @@ #define K_ENOTEMPTY 39 #define K_ELOOP 40 #define K_EWOULDBLOCK K_EAGAIN -#define K_ENODATA 61 -#define K_EBADFD 77 +#define K_ENODATA 61 +#define K_EOVERFLOW 75 +#define K_EBADFD 77 #define K_ENOTSOCK 88 #define K_EDESTADDRREQ 89 #define K_EMSGSIZE 90 @@ -86,9 +87,10 @@ #define K_EHOSTDOWN 112 #define K_EHOSTUNREACH 113 #define K_EALREADY 114 -#define K_EINPROGRESS 115 - -#define K_CONTINUE 998 +#define K_EINPROGRESS 115 +#define K_EOWNERDEAD 130 + +#define K_CONTINUE 998 #define K_WAIT 999 -#endif \ No newline at end of file +#endif diff --git a/include/kobject.h b/include/kobject.h index 69f3ae00e..6ccabada1 100644 --- a/include/kobject.h +++ b/include/kobject.h @@ -24,11 +24,12 @@ #define KTYPE_NATIVE_SOCKET 2 #define KTYPE_EPOLL 3 #define KTYPE_SIGNAL 4 -#define KTYPE_TIMER 5 -#define KTYPE_EVENT 6 - -// can be shared between processes (see kunixsocket sendmsg/recvmsg) but in each process they will have their own file descriptor -class KObject : public std::enable_shared_from_this { +#define KTYPE_TIMER 5 +#define KTYPE_EVENT 6 +#define KTYPE_NTSYNC 7 + +// can be shared between processes (see kunixsocket sendmsg/recvmsg) but in each process they will have their own file descriptor +class KObject : public std::enable_shared_from_this { protected: KObject(U32 type); public: @@ -63,4 +64,4 @@ class KObject : public std::enable_shared_from_this { U32 type; }; -#endif \ No newline at end of file +#endif diff --git a/source/kernel/devs/devntsync.cpp b/source/kernel/devs/devntsync.cpp new file mode 100644 index 000000000..f40c02f2d --- /dev/null +++ b/source/kernel/devs/devntsync.cpp @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2012-2026 The BoxedWine Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "boxedwine.h" +#include "devntsync.h" + +#include "../../io/fsvirtualopennode.h" + +struct NTSyncContext { + NTSyncContext() : cond(std::make_shared(B("NTSyncContext::cond"))) {} + + BOXEDWINE_MUTEX mutex; + BOXEDWINE_CONDITION cond; +}; + +struct NTSyncSemArgs { + U32 count; + U32 max; +}; + +struct NTSyncMutexArgs { + U32 owner; + U32 count; +}; + +struct NTSyncEventArgs { + U32 manual; + U32 signaled; +}; + +enum class NTSyncObjectType { + Semaphore, + Mutex, + Event +}; + +static constexpr U32 NTSYNC_IOC_CREATE_SEM = 0x40084e80; +static constexpr U32 NTSYNC_IOC_SEM_RELEASE = 0xc0044e81; +static constexpr U32 NTSYNC_IOC_WAIT_ANY = 0xc0284e82; +static constexpr U32 NTSYNC_IOC_WAIT_ALL = 0xc0284e83; +static constexpr U32 NTSYNC_IOC_CREATE_MUTEX = 0x40084e84; +static constexpr U32 NTSYNC_IOC_MUTEX_UNLOCK = 0xc0084e85; +static constexpr U32 NTSYNC_IOC_MUTEX_KILL = 0x40044e86; +static constexpr U32 NTSYNC_IOC_CREATE_EVENT = 0x40084e87; +static constexpr U32 NTSYNC_IOC_EVENT_SET = 0x80044e88; +static constexpr U32 NTSYNC_IOC_EVENT_RESET = 0x80044e89; +static constexpr U32 NTSYNC_IOC_EVENT_PULSE = 0x80044e8a; +static constexpr U32 NTSYNC_IOC_SEM_READ = 0x80084e8b; +static constexpr U32 NTSYNC_IOC_MUTEX_READ = 0x80084e8c; +static constexpr U32 NTSYNC_IOC_EVENT_READ = 0x80084e8d; + +static bool canRead(KThread* thread, U32 address, U32 len) { + return address && thread->memory->canRead(address, len); +} + +static bool canWrite(KThread* thread, U32 address, U32 len) { + return address && thread->memory->canWrite(address, len); +} + +static NTSyncSemArgs readSemArgs(KThread* thread, U32 address) { + return { thread->memory->readd(address), thread->memory->readd(address + 4) }; +} + +static NTSyncMutexArgs readMutexArgs(KThread* thread, U32 address) { + return { thread->memory->readd(address), thread->memory->readd(address + 4) }; +} + +static NTSyncEventArgs readEventArgs(KThread* thread, U32 address) { + return { thread->memory->readd(address), thread->memory->readd(address + 4) }; +} + +static void writeSemArgs(KThread* thread, U32 address, const NTSyncSemArgs& args) { + thread->memory->writed(address, args.count); + thread->memory->writed(address + 4, args.max); +} + +static void writeMutexArgs(KThread* thread, U32 address, const NTSyncMutexArgs& args) { + thread->memory->writed(address, args.owner); + thread->memory->writed(address + 4, args.count); +} + +static void writeEventArgs(KThread* thread, U32 address, const NTSyncEventArgs& args) { + thread->memory->writed(address, args.manual); + thread->memory->writed(address + 4, args.signaled); +} + +class KNTSyncObject : public KObject { +public: + KNTSyncObject(const std::shared_ptr& context, const NTSyncSemArgs& args) + : KObject(KTYPE_NTSYNC), context(context), objectType(NTSyncObjectType::Semaphore), sem(args) {} + + KNTSyncObject(const std::shared_ptr& context, const NTSyncMutexArgs& args) + : KObject(KTYPE_NTSYNC), context(context), objectType(NTSyncObjectType::Mutex), mutex(args) {} + + KNTSyncObject(const std::shared_ptr& context, const NTSyncEventArgs& args) + : KObject(KTYPE_NTSYNC), context(context), objectType(NTSyncObjectType::Event), event(args) { + event.signaled = event.signaled ? 1 : 0; + event.manual = event.manual ? 1 : 0; + } + + U32 ioctl(KThread* thread, U32 request) override; + S64 seek(S64 pos) override { return 0; } + S64 length() override { return 0; } + S64 getPos() override { return 0; } + void setBlocking(bool blocking) override {} + bool isBlocking() override { return false; } + void setAsync(bool isAsync) override {} + bool isAsync() override { return false; } + KFileLock* getLock(KFileLock* lock) override { return nullptr; } + U32 setLock(KFileLock* lock, bool wait) override { return -K_ENOLCK; } + bool supportsLocks() override { return false; } + bool isOpen() override { return true; } + bool isReadReady() override { return false; } + bool isWriteReady() override { return false; } + void waitForEvents(BOXEDWINE_CONDITION& parentCondition, U32 events) override {} + U32 writeNative(U8* buffer, U32 len) override { return -K_EINVAL; } + U32 readNative(U8* buffer, U32 len) override { return -K_EINVAL; } + U32 stat(KProcess* process, U32 address, bool is64) override { return -K_ENODEV; } + U32 map(KThread* thread, U32 address, U32 len, S32 prot, S32 flags, U64 off) override { return -K_ENODEV; } + bool canMap() override { return false; } + BString selfFd() override { return B("/dev/ntsync/object"); } + +private: + std::shared_ptr context; + NTSyncObjectType objectType; + NTSyncSemArgs sem = {}; + NTSyncMutexArgs mutex = {}; + NTSyncEventArgs event = {}; + bool mutexAbandoned = false; +}; + +U32 KNTSyncObject::ioctl(KThread* thread, U32 request) { + U32 address = IOCTL_ARG1; + + BOXEDWINE_CRITICAL_SECTION_WITH_MUTEX(context->mutex); + + switch (request) { + case NTSYNC_IOC_SEM_RELEASE: { + if (objectType != NTSyncObjectType::Semaphore) { + return -K_EINVAL; + } + if (!canRead(thread, address, 4) || !canWrite(thread, address, 4)) { + return -K_EFAULT; + } + U32 release = thread->memory->readd(address); + U32 previous = sem.count; + if (release > sem.max || sem.count > sem.max - release) { + return -K_EOVERFLOW; + } + sem.count += release; + thread->memory->writed(address, previous); + BOXEDWINE_CONDITION_SIGNAL_ALL(context->cond); + return 0; + } + case NTSYNC_IOC_SEM_READ: + if (objectType != NTSyncObjectType::Semaphore) { + return -K_EINVAL; + } + if (!canWrite(thread, address, 8)) { + return -K_EFAULT; + } + writeSemArgs(thread, address, sem); + return 0; + case NTSYNC_IOC_MUTEX_UNLOCK: { + if (objectType != NTSyncObjectType::Mutex) { + return -K_EINVAL; + } + if (!canRead(thread, address, 8) || !canWrite(thread, address, 8)) { + return -K_EFAULT; + } + NTSyncMutexArgs args = readMutexArgs(thread, address); + if (!args.owner) { + return -K_EINVAL; + } + if (mutex.owner != args.owner || !mutex.count) { + return -K_EPERM; + } + U32 previousCount = mutex.count; + mutex.count--; + if (!mutex.count) { + mutex.owner = 0; + BOXEDWINE_CONDITION_SIGNAL_ALL(context->cond); + } + args.count = previousCount; + writeMutexArgs(thread, address, args); + return 0; + } + case NTSYNC_IOC_MUTEX_KILL: { + if (objectType != NTSyncObjectType::Mutex) { + return -K_EINVAL; + } + if (!canRead(thread, address, 4)) { + return -K_EFAULT; + } + U32 owner = thread->memory->readd(address); + if (!owner) { + return -K_EINVAL; + } + if (mutex.owner != owner || !mutex.count) { + return -K_EPERM; + } + mutex.owner = 0; + mutex.count = 0; + mutexAbandoned = true; + BOXEDWINE_CONDITION_SIGNAL_ALL(context->cond); + return 0; + } + case NTSYNC_IOC_MUTEX_READ: + if (objectType != NTSyncObjectType::Mutex) { + return -K_EINVAL; + } + if (!canWrite(thread, address, 8)) { + return -K_EFAULT; + } + if (mutexAbandoned) { + writeMutexArgs(thread, address, {}); + return -K_EOWNERDEAD; + } + writeMutexArgs(thread, address, mutex); + return 0; + case NTSYNC_IOC_EVENT_SET: { + if (objectType != NTSyncObjectType::Event) { + return -K_EINVAL; + } + if (!canWrite(thread, address, 4)) { + return -K_EFAULT; + } + U32 previous = event.signaled; + event.signaled = 1; + thread->memory->writed(address, previous); + BOXEDWINE_CONDITION_SIGNAL_ALL(context->cond); + return 0; + } + case NTSYNC_IOC_EVENT_RESET: { + if (objectType != NTSyncObjectType::Event) { + return -K_EINVAL; + } + if (!canWrite(thread, address, 4)) { + return -K_EFAULT; + } + U32 previous = event.signaled; + event.signaled = 0; + thread->memory->writed(address, previous); + return 0; + } + case NTSYNC_IOC_EVENT_PULSE: { + if (objectType != NTSyncObjectType::Event) { + return -K_EINVAL; + } + if (!canWrite(thread, address, 4)) { + return -K_EFAULT; + } + U32 previous = event.signaled; + event.signaled = 0; + thread->memory->writed(address, previous); + BOXEDWINE_CONDITION_SIGNAL_ALL(context->cond); + return 0; + } + case NTSYNC_IOC_EVENT_READ: + if (objectType != NTSyncObjectType::Event) { + return -K_EINVAL; + } + if (!canWrite(thread, address, 8)) { + return -K_EFAULT; + } + writeEventArgs(thread, address, event); + return 0; + default: + return -K_ENOTTY; + } +} + +class DevNTSync : public FsVirtualOpenNode { +public: + DevNTSync(const std::shared_ptr& node, U32 flags) : FsVirtualOpenNode(node, flags), context(std::make_shared()) {} + + U32 ioctl(KThread* thread, U32 request) override; + U32 readNative(U8* buffer, U32 len) override { return -K_EINVAL; } + U32 writeNative(U8* buffer, U32 len) override { return -K_EINVAL; } + +private: + U32 createSemaphore(KThread* thread, U32 address); + U32 createMutex(KThread* thread, U32 address); + U32 createEvent(KThread* thread, U32 address); + + std::shared_ptr context; +}; + +U32 DevNTSync::createSemaphore(KThread* thread, U32 address) { + if (!canRead(thread, address, 8)) { + return -K_EFAULT; + } + NTSyncSemArgs args = readSemArgs(thread, address); + if (args.count > args.max) { + return -K_EINVAL; + } + std::shared_ptr object = std::make_shared(context, args); + return thread->process->allocFileDescriptor(object, K_O_RDWR, 0, -1, 0)->handle; +} + +U32 DevNTSync::createMutex(KThread* thread, U32 address) { + if (!canRead(thread, address, 8)) { + return -K_EFAULT; + } + NTSyncMutexArgs args = readMutexArgs(thread, address); + if ((!args.owner && args.count) || (args.owner && !args.count)) { + return -K_EINVAL; + } + std::shared_ptr object = std::make_shared(context, args); + return thread->process->allocFileDescriptor(object, K_O_RDWR, 0, -1, 0)->handle; +} + +U32 DevNTSync::createEvent(KThread* thread, U32 address) { + if (!canRead(thread, address, 8)) { + return -K_EFAULT; + } + NTSyncEventArgs args = readEventArgs(thread, address); + std::shared_ptr object = std::make_shared(context, args); + return thread->process->allocFileDescriptor(object, K_O_RDWR, 0, -1, 0)->handle; +} + +U32 DevNTSync::ioctl(KThread* thread, U32 request) { + U32 address = IOCTL_ARG1; + + switch (request) { + case NTSYNC_IOC_CREATE_SEM: + return createSemaphore(thread, address); + case NTSYNC_IOC_CREATE_MUTEX: + return createMutex(thread, address); + case NTSYNC_IOC_CREATE_EVENT: + return createEvent(thread, address); + case NTSYNC_IOC_WAIT_ANY: + case NTSYNC_IOC_WAIT_ALL: + return -K_ENOSYS; + default: + return -K_ENOTTY; + } +} + +FsOpenNode* openDevNTSync(const std::shared_ptr& node, U32 flags, U32 data) { + return new DevNTSync(node, flags); +} diff --git a/source/sdl/startupArgs.cpp b/source/sdl/startupArgs.cpp index e64e39a95..881340637 100644 --- a/source/sdl/startupArgs.cpp +++ b/source/sdl/startupArgs.cpp @@ -32,10 +32,11 @@ #include "procstat.h" #include "uptime.h" -#include "devmixer.h" -#include "devsequencer.h" -#include "devfb.h" -#include "mainloop.h" +#include "devmixer.h" +#include "devsequencer.h" +#include "devfb.h" +#include "devntsync.h" +#include "mainloop.h" #include "../io/fsfilenode.h" #include "../io/fszip.h" #include "loader.h" @@ -124,9 +125,10 @@ void StartUpArgs::buildVirtualFileSystem() { Fs::addVirtualFile(B("/dev/tty2"), openDevTTY, K__S_IREAD|K__S_IWRITE|K__S_IFCHR, k_mdev(4, 2), devNode); // used by XOrg Fs::addVirtualFile(B("/dev/urandom"), openDevURandom, K__S_IREAD|K__S_IFCHR, k_mdev(1, 9), devNode); Fs::addVirtualFile(B("/dev/random"), openDevURandom, K__S_IREAD|K__S_IFCHR, k_mdev(1, 8), devNode); - Fs::addVirtualFile(B("/dev/null"), openDevNull, K__S_IREAD|K__S_IWRITE|K__S_IFCHR, k_mdev(1, 3), devNode); - Fs::addVirtualFile(B("/dev/zero"), openDevZero, K__S_IREAD|K__S_IWRITE|K__S_IFCHR, k_mdev(1, 5), devNode); - Fs::addVirtualFile(B("/proc/meminfo"), openMemInfo, K__S_IREAD, k_mdev(0, 0), KSystem::procNode); + Fs::addVirtualFile(B("/dev/null"), openDevNull, K__S_IREAD|K__S_IWRITE|K__S_IFCHR, k_mdev(1, 3), devNode); + Fs::addVirtualFile(B("/dev/zero"), openDevZero, K__S_IREAD|K__S_IWRITE|K__S_IFCHR, k_mdev(1, 5), devNode); + Fs::addVirtualFile(B("/dev/ntsync"), openDevNTSync, K__S_IREAD | K__S_IWRITE | K__S_IFCHR, k_mdev(10, 59), devNode); + Fs::addVirtualFile(B("/proc/meminfo"), openMemInfo, K__S_IREAD, k_mdev(0, 0), KSystem::procNode); Fs::addVirtualFile(B("/proc/stat"), openProcStat, K__S_IREAD, k_mdev(0, 0), KSystem::procNode); Fs::addVirtualFile(B("/proc/uptime"), openUptime, K__S_IREAD, k_mdev(0, 0), KSystem::procNode); Fs::addVirtualFile(B("/proc/cpuinfo"), openCpuInfo, K__S_IREAD, k_mdev(0, 0), KSystem::procNode); From bcd236f5258887c425314da49734ff346ebc7e13 Mon Sep 17 00:00:00 2001 From: kevodwyer Date: Tue, 16 Jun 2026 11:54:35 +0100 Subject: [PATCH 3/3] feat(ntsync): implement emulated /dev/ntsync device Implement the full ioctl surface and wait semantics for the emulated NTSYNC backend: semaphores, mutexes, events, and WAIT_ANY/WAIT_ALL with alert, absolute timeouts, abandoned-mutex EOWNERDEAD, and duplicate rejection. EVENT_PULSE uses an exact per-context waiter registry (auto-reset wakes one, manual-reset wakes all, no stale grants). Works in both multi-threaded and single-threaded builds. --- docs/NTSYNC-Implementation-Plan.md | 151 +++++++---- source/kernel/devs/devntsync.cpp | 421 ++++++++++++++++++++++++++++- 2 files changed, 515 insertions(+), 57 deletions(-) diff --git a/docs/NTSYNC-Implementation-Plan.md b/docs/NTSYNC-Implementation-Plan.md index 7acc68d03..2a58dde99 100644 --- a/docs/NTSYNC-Implementation-Plan.md +++ b/docs/NTSYNC-Implementation-Plan.md @@ -68,87 +68,105 @@ References: Implement the ioctl constants and structures from `linux/ntsync.h`. -- [ ] `NTSYNC_IOC_CREATE_SEM` -- [ ] `NTSYNC_IOC_CREATE_MUTEX` -- [ ] `NTSYNC_IOC_CREATE_EVENT` -- [ ] `NTSYNC_IOC_SEM_RELEASE` -- [ ] `NTSYNC_IOC_SEM_READ` -- [ ] `NTSYNC_IOC_MUTEX_UNLOCK` -- [ ] `NTSYNC_IOC_MUTEX_KILL` -- [ ] `NTSYNC_IOC_MUTEX_READ` -- [ ] `NTSYNC_IOC_EVENT_SET` -- [ ] `NTSYNC_IOC_EVENT_RESET` -- [ ] `NTSYNC_IOC_EVENT_PULSE` -- [ ] `NTSYNC_IOC_EVENT_READ` -- [ ] `NTSYNC_IOC_WAIT_ANY` -- [ ] `NTSYNC_IOC_WAIT_ALL` +- [x] `NTSYNC_IOC_CREATE_SEM` +- [x] `NTSYNC_IOC_CREATE_MUTEX` +- [x] `NTSYNC_IOC_CREATE_EVENT` +- [x] `NTSYNC_IOC_SEM_RELEASE` +- [x] `NTSYNC_IOC_SEM_READ` +- [x] `NTSYNC_IOC_MUTEX_UNLOCK` +- [x] `NTSYNC_IOC_MUTEX_KILL` +- [x] `NTSYNC_IOC_MUTEX_READ` +- [x] `NTSYNC_IOC_EVENT_SET` +- [x] `NTSYNC_IOC_EVENT_RESET` +- [x] `NTSYNC_IOC_EVENT_PULSE` +- [x] `NTSYNC_IOC_EVENT_READ` +- [x] `NTSYNC_IOC_WAIT_ANY` +- [x] `NTSYNC_IOC_WAIT_ALL` + +All ioctls are implemented in the emulated backend (`source/kernel/devs/devntsync.cpp`). ## Semantic Requirements -- [ ] Semaphore count must never exceed max; overflow returns `EOVERFLOW`. -- [ ] Acquiring a semaphore decrements its count. -- [ ] Mutex owner zero means unowned. -- [ ] Mutex recursion count and owner must be consistent at create time. -- [ ] Unlock by owner zero returns `EINVAL`. -- [ ] Unlock by non-owner returns `EPERM`. -- [ ] Killed mutexes become abandoned and unowned. -- [ ] Acquiring an abandoned mutex returns `EOWNERDEAD` while still acquiring it. -- [ ] Auto-reset events become unsignaled when acquired. -- [ ] Manual-reset events remain signaled when acquired. -- [ ] Pulse wakes eligible waiters and leaves the event unsignaled. -- [ ] `WAIT_ANY` acquires at most one object and writes the selected index. -- [ ] `WAIT_ALL` acquires all objects atomically or modifies none. -- [ ] `WAIT_ALL` rejects duplicate objects as the kernel API specifies. -- [ ] Timeouts use absolute nanoseconds and honor `NTSYNC_WAIT_REALTIME`. -- [ ] Signals and thread termination return appropriate interrupt behavior. +- [x] Semaphore count must never exceed max; overflow returns `EOVERFLOW`. +- [x] Acquiring a semaphore decrements its count. +- [x] Mutex owner zero means unowned. +- [x] Mutex recursion count and owner must be consistent at create time. +- [x] Unlock by owner zero returns `EINVAL`. +- [x] Unlock by non-owner returns `EPERM`. +- [x] Killed mutexes become abandoned and unowned. +- [x] Acquiring an abandoned mutex returns `EOWNERDEAD` while still acquiring it. +- [x] Auto-reset events become unsignaled when acquired. +- [x] Manual-reset events remain signaled when acquired. +- [x] Pulse wakes eligible waiters and leaves the event unsignaled. Each context + keeps a registry of blocked waiters (`NTSyncContext::waiters`, objects held + weakly to avoid reference cycles). A pulse momentarily signals the event and + walks the registry in arrival order, acquiring objects on behalf of each + eligible waiter and recording the wake result. Acquiring an auto-reset event + designals it, so only the first waiter wakes; a manual-reset event stays + signaled across the walk, so all eligible waiters wake. The event is then + reset and no state lingers, so waiters that arrive after the pulse are not + affected. A pulse with no blocked waiters wakes no one, as expected. +- [x] `WAIT_ANY` acquires at most one object and writes the selected index. +- [x] `WAIT_ALL` acquires all objects atomically or modifies none. +- [x] `WAIT_ALL` rejects duplicate objects (in `objs`, or shared with `alert`). +- [x] Timeouts use absolute nanoseconds and honor `NTSYNC_WAIT_REALTIME` + (`U64_MAX` waits forever, a past deadline returns `ETIMEDOUT`). +- [x] Alert event terminates a wait with `index == count`. +- [x] Signals and thread termination return appropriate interrupt behavior + (`EINTR`/`CONTINUE`), mirroring the futex path in `kthread.cpp`. ## Implementation Phases -### Phase 1: Device Discovery +### Phase 1: Device Discovery — done -- [ ] Add `/dev/ntsync` registration. -- [ ] Add placeholder device implementation. -- [ ] Add ioctl constant definitions. -- [ ] Make unsupported ioctls return Linux-compatible errors. -- [ ] Verify Wine opens `/dev/ntsync`. +- [x] Add `/dev/ntsync` registration. +- [x] Add placeholder device implementation. +- [x] Add ioctl constant definitions. +- [x] Make unsupported ioctls return Linux-compatible errors. +- [ ] Verify Wine opens `/dev/ntsync`. (Pending Phase 6 Wine validation.) Acceptance: - [ ] A small guest program can `stat()` and `open()` `/dev/ntsync`. - [ ] Wine 11 attempts NTSYNC ioctls instead of immediately falling back. -### Phase 2: Object Creation +### Phase 2: Object Creation — done -- [ ] Implement create semaphore. -- [ ] Implement create mutex. -- [ ] Implement create event. -- [ ] Return valid guest FDs for created objects. -- [ ] Reject invalid create arguments. +- [x] Implement create semaphore. +- [x] Implement create mutex. +- [x] Implement create event. +- [x] Return valid guest FDs for created objects. +- [x] Reject invalid create arguments. Acceptance: - [ ] Guest test can create each object type and close returned FDs. - [ ] Invalid create inputs return expected errors. -### Phase 3: Read and State Mutation +### Phase 3: Read and State Mutation — done -- [ ] Implement semaphore release/read. -- [ ] Implement mutex unlock/kill/read. -- [ ] Implement event set/reset/pulse/read. -- [ ] Wake waiters when object state changes. +- [x] Implement semaphore release/read. +- [x] Implement mutex unlock/kill/read. +- [x] Implement event set/reset/pulse/read. +- [x] Wake waiters when object state changes (single per-context condition). Acceptance: - [ ] Unit tests cover state transitions for every object type. - [ ] Error returns match Linux NTSYNC behavior. -### Phase 4: Wait Semantics +### Phase 4: Wait Semantics — done (emulated backend) -- [ ] Implement `WAIT_ANY`. -- [ ] Implement `WAIT_ALL`. -- [ ] Implement alert event handling. -- [ ] Implement timeout handling. -- [ ] Implement abandoned mutex results during waits. +- [x] Implement `WAIT_ANY` (lowest signaled index wins). +- [x] Implement `WAIT_ALL` (atomic acquire-all, duplicate rejection). +- [x] Implement alert event handling (`index == count`). +- [x] Implement timeout handling (absolute ns, monotonic/realtime, infinite). +- [x] Implement abandoned mutex results during waits (`EOWNERDEAD`). + +The wait loop mirrors the futex implementation in `source/kernel/kthread.cpp`: +it blocks on the per-context condition and works in both the multi-threaded and +the cooperative/single-threaded (wasm) builds. Because the NTSYNC timeout is +absolute, recomputing the deadline after a cooperative re-entry stays correct. Acceptance: @@ -203,3 +221,30 @@ Acceptance: ## Progress Log - 2026-05-21: Initial plan recorded after reviewing Boxedwine syscall/device structure and Linux NTSYNC uAPI. +- 2026-06-15: Completed the emulated backend (Phases 1–4). + - Fixed a build break in the WIP device: the ioctl handlers used `IOCTL_ARG1` + (`EDX`) without a `CPU* cpu` in scope. + - Unified all object state access, signaling and waiting under the single + per-context `BoxedWineCondition` to remove the lost-wakeup race between the + state-changing ioctls and waiters (previously a separate `context->mutex` + was used for mutation while waits would have needed the condition mutex). + - Implemented `NTSYNC_IOC_WAIT_ANY` and `NTSYNC_IOC_WAIT_ALL` including alert + events, absolute timeouts (monotonic/realtime/infinite), abandoned-mutex + `EOWNERDEAD`, duplicate rejection for `WAIT_ALL`, and signal/termination + interrupt handling. Works in both threading models. + - Added per-type `selfFd()` labels for `/proc/self/fd`. +- 2026-06-15 (later): Reworked `EVENT_PULSE` from the initial bounded grant model + to an exact, kernel-faithful implementation. Each context now keeps a registry + of blocked waiters; a pulse walks it, momentarily signaling the event, and + acquires objects on behalf of each eligible waiter (auto-reset wakes one, + manual-reset wakes all). This removes the stale-grant edge cases where a later + waiter could be wrongly satisfied. The registry holds objects weakly so a + lingering record can never keep a context/object alive. + - Verified the full Linux release build compiles and links; the device file + also syntax-checks under the single-threaded (non-`BOXEDWINE_MULTI_THREADED`) + configuration. + - Not yet done: Phase 5 (native host `/dev/ntsync` acceleration — the dev host + runs kernel 6.8 without `/dev/ntsync`, so it cannot be exercised here) and + Phase 6 (Wine 11 validation/benchmarking). Automated tests are still pending; + Boxedwine's test harness is CPU/MMU-focused and has no guest-syscall + fixture, so a guest-side NTSYNC test program is the likely vehicle. diff --git a/source/kernel/devs/devntsync.cpp b/source/kernel/devs/devntsync.cpp index f40c02f2d..f4bdc0995 100644 --- a/source/kernel/devs/devntsync.cpp +++ b/source/kernel/devs/devntsync.cpp @@ -12,11 +12,45 @@ #include "../../io/fsvirtualopennode.h" +// Emulated backend for Wine's NTSYNC path (/dev/ntsync). The ABI mirrors the +// Linux uAPI in include/uapi/linux/ntsync.h. Each open of /dev/ntsync creates +// one NTSyncContext; objects created from a context can only be waited on +// together with objects from the same context. + +class KNTSyncObject; + +// One record per thread currently blocked inside a WAIT_ANY/WAIT_ALL on this +// context. The registry lets NTSYNC_IOC_EVENT_PULSE faithfully wake exactly the +// waiters that are blocked at pulse time (see KNTSyncObject::ioctl). Objects are +// held weakly so a registered waiter never keeps its objects (and, through them, +// the context) alive: a stale record can never form a reference cycle. +struct NTSyncWaiter { + U32 threadId = 0; + U32 owner = 0; + bool waitAll = false; + bool hasAlert = false; + std::vector> objs; + std::weak_ptr alert; + + // Set when a pulse acquired this waiter's objects on its behalf. The blocked + // thread reports the recorded index/result on its next wake without acquiring + // anything again. + bool pulseSatisfied = false; + U32 pulseIndex = 0; + U32 pulseResult = 0; +}; + struct NTSyncContext { NTSyncContext() : cond(std::make_shared(B("NTSyncContext::cond"))) {} - BOXEDWINE_MUTEX mutex; + // All object state, signaling and waiting is serialized through this single + // per-context condition. State changing ioctls signal it; waiters block on + // it and re-evaluate object state when woken. BOXEDWINE_CONDITION cond; + + // Active waiters, in arrival order. Only touched while the condition lock is + // held. + std::vector> waiters; }; struct NTSyncSemArgs { @@ -55,6 +89,21 @@ static constexpr U32 NTSYNC_IOC_SEM_READ = 0x80084e8b; static constexpr U32 NTSYNC_IOC_MUTEX_READ = 0x80084e8c; static constexpr U32 NTSYNC_IOC_EVENT_READ = 0x80084e8d; +static constexpr U32 NTSYNC_MAX_WAIT_COUNT = 64; +static constexpr U32 NTSYNC_WAIT_REALTIME = 0x1; +static constexpr U64 NTSYNC_TIMEOUT_INFINITE = 0xFFFFFFFFFFFFFFFFULL; + +// struct ntsync_wait_args layout (40 bytes) +static constexpr U32 NTSYNC_WAIT_OFF_TIMEOUT = 0; // __u64 +static constexpr U32 NTSYNC_WAIT_OFF_OBJS = 8; // __u64 (guest pointer) +static constexpr U32 NTSYNC_WAIT_OFF_COUNT = 16; // __u32 +static constexpr U32 NTSYNC_WAIT_OFF_INDEX = 20; // __u32 (out) +static constexpr U32 NTSYNC_WAIT_OFF_FLAGS = 24; // __u32 +static constexpr U32 NTSYNC_WAIT_OFF_OWNER = 28; // __u32 +static constexpr U32 NTSYNC_WAIT_OFF_ALERT = 32; // __u32 +static constexpr U32 NTSYNC_WAIT_OFF_PAD = 36; // __u32 +static constexpr U32 NTSYNC_WAIT_ARGS_SIZE = 40; + static bool canRead(KThread* thread, U32 address, U32 len) { return address && thread->memory->canRead(address, len); } @@ -124,7 +173,13 @@ class KNTSyncObject : public KObject { U32 stat(KProcess* process, U32 address, bool is64) override { return -K_ENODEV; } U32 map(KThread* thread, U32 address, U32 len, S32 prot, S32 flags, U64 off) override { return -K_ENODEV; } bool canMap() override { return false; } - BString selfFd() override { return B("/dev/ntsync/object"); } + BString selfFd() override; + + // The following helpers operate on object state and must be called while the + // owning context's condition lock is held (see DevNTSync::wait). + bool sameContext(NTSyncContext* other) const { return context.get() == other; } + bool waitSignaled(U32 owner) const; + U32 waitAcquire(U32 owner); // returns 0 or -K_EOWNERDEAD; only call when waitSignaled() is true private: std::shared_ptr context; @@ -135,10 +190,129 @@ class KNTSyncObject : public KObject { bool mutexAbandoned = false; }; +BString KNTSyncObject::selfFd() { + switch (objectType) { + case NTSyncObjectType::Semaphore: return B("/dev/ntsync/semaphore"); + case NTSyncObjectType::Mutex: return B("/dev/ntsync/mutex"); + case NTSyncObjectType::Event: return B("/dev/ntsync/event"); + } + return B("/dev/ntsync/object"); +} + +bool KNTSyncObject::waitSignaled(U32 owner) const { + switch (objectType) { + case NTSyncObjectType::Semaphore: + return sem.count > 0; + case NTSyncObjectType::Mutex: + // signaled if unowned or already owned by this owner, unless the + // recursion count would overflow + return (mutex.owner == 0 || mutex.owner == owner) && mutex.count < 0xFFFFFFFF; + case NTSyncObjectType::Event: + return event.signaled != 0; + } + return false; +} + +U32 KNTSyncObject::waitAcquire(U32 owner) { + switch (objectType) { + case NTSyncObjectType::Semaphore: + sem.count--; + return 0; + case NTSyncObjectType::Mutex: + mutex.owner = owner; + mutex.count++; + if (mutexAbandoned) { + mutexAbandoned = false; + return -K_EOWNERDEAD; + } + return 0; + case NTSyncObjectType::Event: + if (event.signaled && !event.manual) { + event.signaled = 0; + } + return 0; + } + return 0; +} + +// Try to satisfy a wait over the given resolved objects. On success acquires the +// relevant object(s), stores the index to report and the ioctl result (0 or +// -K_EOWNERDEAD), and returns true. Does not touch guest memory. Must be called +// with the context lock held. +static bool ntsyncTryAcquire(const std::vector>& objs, + const std::shared_ptr& alert, + U32 owner, bool waitAll, U32& index, U32& result) { + U32 count = (U32)objs.size(); + + if (waitAll) { + bool all = true; + for (auto& obj : objs) { + if (!obj->waitSignaled(owner)) { + all = false; + break; + } + } + if (all) { + // Acquire every object atomically. EOWNERDEAD is reported if any of + // the acquired mutexes was abandoned. + result = 0; + for (auto& obj : objs) { + U32 r = obj->waitAcquire(owner); + if (r) { + result = r; + } + } + index = 0; + return true; + } + } else { + for (U32 i = 0; i < count; i++) { + if (objs[i]->waitSignaled(owner)) { + result = objs[i]->waitAcquire(owner); + index = i; + return true; + } + } + } + + if (alert && alert->waitSignaled(owner)) { + alert->waitAcquire(owner); + index = count; + result = 0; + return true; + } + return false; +} + +// Resolve a registered waiter's weak object references into strong ones. +// Returns false (waiter unsatisfiable) if any referenced object has been +// destroyed since it registered. +static bool lockWaiterObjs(const NTSyncWaiter& waiter, std::vector>& objs, + std::shared_ptr& alert) { + objs.clear(); + for (auto& weak : waiter.objs) { + std::shared_ptr obj = weak.lock(); + if (!obj) { + return false; + } + objs.push_back(std::move(obj)); + } + if (waiter.hasAlert) { + alert = waiter.alert.lock(); + if (!alert) { + return false; + } + } else { + alert = nullptr; + } + return true; +} + U32 KNTSyncObject::ioctl(KThread* thread, U32 request) { + CPU* cpu = thread->cpu; U32 address = IOCTL_ARG1; - BOXEDWINE_CRITICAL_SECTION_WITH_MUTEX(context->mutex); + BOXEDWINE_CRITICAL_SECTION_WITH_CONDITION(context->cond); switch (request) { case NTSYNC_IOC_SEM_RELEASE: { @@ -257,6 +431,32 @@ U32 KNTSyncObject::ioctl(KThread* thread, U32 request) { return -K_EFAULT; } U32 previous = event.signaled; + // A pulse momentarily signals the event, releases the waiters that are + // eligible right now, and leaves the event unsignaled. We replicate the + // kernel behavior by signaling the event and walking the registered + // waiters in arrival order: each eligible waiter's objects are acquired + // on its behalf and the wake result is recorded. Because acquiring an + // auto-reset event designals it, only the first waiter wakes; a + // manual-reset event stays signaled across the walk, so every eligible + // waiter wakes. The event is then reset regardless of its prior state. + event.signaled = 1; + for (auto& waiter : context->waiters) { + if (waiter->pulseSatisfied) { + continue; + } + std::vector> objs; + std::shared_ptr alert; + if (!lockWaiterObjs(*waiter, objs, alert)) { + continue; + } + U32 index = 0; + U32 result = 0; + if (ntsyncTryAcquire(objs, alert, waiter->owner, waiter->waitAll, index, result)) { + waiter->pulseSatisfied = true; + waiter->pulseIndex = index; + waiter->pulseResult = result; + } + } event.signaled = 0; thread->memory->writed(address, previous); BOXEDWINE_CONDITION_SIGNAL_ALL(context->cond); @@ -288,6 +488,15 @@ class DevNTSync : public FsVirtualOpenNode { U32 createSemaphore(KThread* thread, U32 address); U32 createMutex(KThread* thread, U32 address); U32 createEvent(KThread* thread, U32 address); + U32 wait(KThread* thread, U32 address, bool waitAll); + + std::shared_ptr resolve(KThread* thread, S32 fd); + + // Waiter registry helpers. All must be called with the context lock held. + std::shared_ptr findWaiter(U32 threadId); + void removeWaiter(U32 threadId); + void registerWaiter(U32 threadId, const std::vector>& objs, + const std::shared_ptr& alert, U32 owner, bool waitAll); std::shared_ptr context; }; @@ -325,7 +534,210 @@ U32 DevNTSync::createEvent(KThread* thread, U32 address) { return thread->process->allocFileDescriptor(object, K_O_RDWR, 0, -1, 0)->handle; } +std::shared_ptr DevNTSync::resolve(KThread* thread, S32 fd) { + KFileDescriptorPtr desc = thread->process->getFileDescriptor(fd); + if (!desc || desc->kobject->type != KTYPE_NTSYNC) { + return nullptr; + } + std::shared_ptr obj = std::dynamic_pointer_cast(desc->kobject); + if (!obj || !obj->sameContext(context.get())) { + return nullptr; + } + return obj; +} + +std::shared_ptr DevNTSync::findWaiter(U32 threadId) { + for (auto& waiter : context->waiters) { + if (waiter->threadId == threadId) { + return waiter; + } + } + return nullptr; +} + +void DevNTSync::removeWaiter(U32 threadId) { + std::vector>& waiters = context->waiters; + for (size_t i = 0; i < waiters.size(); i++) { + if (waiters[i]->threadId == threadId) { + waiters.erase(waiters.begin() + i); + return; + } + } +} + +void DevNTSync::registerWaiter(U32 threadId, const std::vector>& objs, + const std::shared_ptr& alert, U32 owner, bool waitAll) { + if (findWaiter(threadId)) { + // Already registered for this wait (a cooperative re-entry or another + // loop iteration); the parameters do not change within one wait. + return; + } + std::shared_ptr waiter = std::make_shared(); + waiter->threadId = threadId; + waiter->owner = owner; + waiter->waitAll = waitAll; + waiter->objs.reserve(objs.size()); + for (auto& obj : objs) { + waiter->objs.push_back(obj); + } + if (alert) { + waiter->alert = alert; + waiter->hasAlert = true; + } + context->waiters.push_back(waiter); +} + +U32 DevNTSync::wait(KThread* thread, U32 address, bool waitAll) { + KMemory* memory = thread->memory; + + BOXEDWINE_CRITICAL_SECTION_WITH_CONDITION(context->cond); + + // If a pulse already acquired this thread's objects, report the recorded + // result without re-validating: the acquisition already happened, so the + // result stands even if a referenced fd has since been closed. + { + std::shared_ptr existing = findWaiter(thread->id); + if (existing && existing->pulseSatisfied) { + memory->writed(address + NTSYNC_WAIT_OFF_INDEX, existing->pulseIndex); + U32 result = existing->pulseResult; + removeWaiter(thread->id); + return result; + } + } + + if (!canRead(thread, address, NTSYNC_WAIT_ARGS_SIZE) || !canWrite(thread, address + NTSYNC_WAIT_OFF_INDEX, 4)) { + removeWaiter(thread->id); + return -K_EFAULT; + } + + U64 timeout = memory->readq(address + NTSYNC_WAIT_OFF_TIMEOUT); + U32 objsPtr = memory->readd(address + NTSYNC_WAIT_OFF_OBJS); + U32 count = memory->readd(address + NTSYNC_WAIT_OFF_COUNT); + U32 flags = memory->readd(address + NTSYNC_WAIT_OFF_FLAGS); + U32 owner = memory->readd(address + NTSYNC_WAIT_OFF_OWNER); + U32 alertFd = memory->readd(address + NTSYNC_WAIT_OFF_ALERT); + U32 pad = memory->readd(address + NTSYNC_WAIT_OFF_PAD); + + if (pad || (flags & ~NTSYNC_WAIT_REALTIME) || count > NTSYNC_MAX_WAIT_COUNT) { + removeWaiter(thread->id); + return -K_EINVAL; + } + if (count && !canRead(thread, objsPtr, count * 4)) { + removeWaiter(thread->id); + return -K_EFAULT; + } + + // Resolve and validate every guest fd. Hold shared_ptr references so the + // objects stay alive for the duration of the (possibly blocking) wait. + std::vector> objs; + objs.reserve(count); + for (U32 i = 0; i < count; i++) { + S32 fd = (S32)memory->readd(objsPtr + i * 4); + std::shared_ptr obj = resolve(thread, fd); + if (!obj) { + removeWaiter(thread->id); + return -K_EINVAL; + } + if (waitAll) { + // WAIT_ALL may not reference the same object more than once. + for (auto& existing : objs) { + if (existing == obj) { + removeWaiter(thread->id); + return -K_EINVAL; + } + } + } + objs.push_back(obj); + } + + std::shared_ptr alert; + if (alertFd) { + alert = resolve(thread, (S32)alertFd); + if (!alert) { + removeWaiter(thread->id); + return -K_EINVAL; + } + if (waitAll) { + for (auto& existing : objs) { + if (existing == alert) { + removeWaiter(thread->id); + return -K_EINVAL; + } + } + } + } + + // The NTSYNC timeout is an absolute value in nanoseconds, measured against + // CLOCK_MONOTONIC unless NTSYNC_WAIT_REALTIME is set. U64_MAX means wait + // forever. Convert to a host millisecond deadline; because the deadline is + // absolute, recomputing it after a cooperative re-entry stays correct. + bool infinite = (timeout == NTSYNC_TIMEOUT_INFINITE); + U32 expireMillis = 0; + if (!infinite) { + U64 nowMicro = (flags & NTSYNC_WAIT_REALTIME) ? KSystem::getSystemTimeAsMicroSeconds() : KSystem::getMicroCounter(); + S64 remainingMicro = (S64)(timeout / 1000ULL) - (S64)nowMicro; + S64 remainingMillis = remainingMicro / 1000; + if (remainingMillis < 0) { + remainingMillis = 0; + } + expireMillis = KSystem::getMilliesSinceStart() + (U32)remainingMillis; + } + + while (true) { + // A pulse may have satisfied us while we were blocked (multi-threaded + // build) or between cooperative re-entries. + std::shared_ptr existing = findWaiter(thread->id); + if (existing && existing->pulseSatisfied) { + memory->writed(address + NTSYNC_WAIT_OFF_INDEX, existing->pulseIndex); + U32 result = existing->pulseResult; + removeWaiter(thread->id); + return result; + } + + if (thread->pendingSignals) { + if (thread->runSignals()) { + removeWaiter(thread->id); + return -K_CONTINUE; + } + } + + U32 index = 0; + U32 result = 0; + if (ntsyncTryAcquire(objs, alert, owner, waitAll, index, result)) { + memory->writed(address + NTSYNC_WAIT_OFF_INDEX, index); + removeWaiter(thread->id); + return result; + } + + // Register before sleeping so a concurrent pulse can release us. + registerWaiter(thread->id, objs, alert, owner, waitAll); + + if (!infinite) { + S32 diff = (S32)(expireMillis - KSystem::getMilliesSinceStart()); + if (diff <= 0) { + removeWaiter(thread->id); + return -K_ETIMEDOUT; + } + BOXEDWINE_CONDITION_WAIT_TIMEOUT(context->cond, (U32)diff); + } else { + BOXEDWINE_CONDITION_WAIT(context->cond); + } +#ifdef BOXEDWINE_MULTI_THREADED + if (thread->terminating) { + removeWaiter(thread->id); + return -K_EINTR; + } + if (thread->startSignal) { + thread->startSignal = false; + removeWaiter(thread->id); + return -K_CONTINUE; + } +#endif + } +} + U32 DevNTSync::ioctl(KThread* thread, U32 request) { + CPU* cpu = thread->cpu; U32 address = IOCTL_ARG1; switch (request) { @@ -336,8 +748,9 @@ U32 DevNTSync::ioctl(KThread* thread, U32 request) { case NTSYNC_IOC_CREATE_EVENT: return createEvent(thread, address); case NTSYNC_IOC_WAIT_ANY: + return wait(thread, address, false); case NTSYNC_IOC_WAIT_ALL: - return -K_ENOSYS; + return wait(thread, address, true); default: return -K_ENOTTY; }