This is server side of SIG handling in OpenBSD.
See ntdll sig for details on why it is done.
To tell what thread is the target tid in an array that is shared with the
process. The Array lives in the file `sig-{pid}` where pid is Windows process
id in hex format %04x.
All 64 slots hold signal/tid pairs. If the table stays full after retries,
the request is dropped without broadcasting.

Index: server/sig_openbsd.c
--- server/sig_openbsd.c.orig
+++ server/sig_openbsd.c
@@ -0,0 +1,135 @@
+/*
+ * Server-side thread signals on OpenBSD
+ *
+ * Copyright 2026 Andrii Yukhymchak
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#include "config.h"
+
+#ifdef __OpenBSD__
+
+#include <fcntl.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#include "ntstatus.h"
+#include "winternl.h"
+#include "file.h"
+#include "process.h"
+#include "request.h"
+#include "sig_openbsd.h"
+#include "thread.h"
+
+/* OpenBSD has no way to signal a given thread on a different process. send_thread_signal() signals
+ * the entire process, and then the thread that gets the signal (random thread) will re-emit that
+ * to the target thread. To tell which thread is the target, the server stores the Unix tid of
+ * the target in an array that is shared with the process. It is stored in the file `sig-{pid}`
+ * of the server directory, where {pid} is Windows process id.
+ *
+ * All 64 entries hold a signal in the high 32 bits and a Unix tid in the low
+ * 32 bits. Zero denotes a free slot; there is no overflow broadcast.
+ *
+ * Has to match forward_thread_signal() in ntdll half of the implementation.
+ */
+
+#define SIGNAL_TARGETS_COUNT 64
+#define SIGNAL_TARGETS_SIZE (SIGNAL_TARGETS_COUNT * sizeof(uint64_t))
+#define SIGNAL_TARGET_RETRIES 100
+
+/* Requests are shared across processes and accessed from a signal handler; atomics must be lock-free. */
+C_ASSERT( __atomic_always_lock_free( sizeof(uint64_t), 0 ) );
+
+static void get_signal_targets_name( struct process *process, char name[16] )
+{
+    snprintf( name, 16, "sig-%04x", process->id );
+}
+
+/* for a new process share an array of signal targets */
+void init_process_signal_targets( struct process *process )
+{
+    char name[16];
+    void *ptr;
+    int fd;
+
+    if (process->signal_targets) return;
+
+    get_signal_targets_name( process, name );
+    /* never reuse a leftover file. Some client might still map it */
+    unlinkat( server_dir_fd, name, 0 );
+    if ((fd = openat( server_dir_fd, name, O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600 )) == -1)
+        return;
+
+    if (ftruncate( fd, SIGNAL_TARGETS_SIZE ) != -1 &&
+        (ptr = mmap( NULL, SIGNAL_TARGETS_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 )) != MAP_FAILED)
+        process->signal_targets = ptr;
+    else
+        unlinkat( server_dir_fd, name, 0 );
+
+    close(fd);
+}
+
+void finish_process_signal_targets( struct process *process )
+{
+    char name[16];
+
+    if (!process->signal_targets) return;
+    munmap( process->signal_targets, SIGNAL_TARGETS_SIZE );
+    process->signal_targets = NULL;
+    get_signal_targets_name( process, name );
+    unlinkat( server_dir_fd, name, 0 );
+}
+
+/* Publish the complete request before notifying its process. */
+int set_signal_target( struct thread *thread, int sig )
+{
+    unsigned int attempt, i;
+    uint64_t request = ((uint64_t)(uint32_t)sig << 32) | (uint32_t)thread->unix_tid;
+    uint64_t *targets = thread->process->signal_targets;
+
+    if (!targets || thread->unix_tid <= 0 ||
+        (sig != SIGINT && sig != SIGUSR1 && sig != SIGQUIT))
+    {
+        fprintf( stderr, "wineserver: warning: cannot queue signal %d for tid %d, ignoring\n",
+                 sig, thread->unix_tid );
+        return 0;
+    }
+
+    for (attempt = 0; attempt < SIGNAL_TARGET_RETRIES; attempt++)
+    {
+        for (i = 0; i < SIGNAL_TARGETS_COUNT; i++)
+        {
+            uint64_t expected = 0;
+
+            if (__atomic_compare_exchange_n( &targets[i], &expected, request, FALSE,
+                                              __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST ))
+                return 1;
+        }
+        sched_yield();
+    }
+    fprintf( stderr, "wineserver: warning: dropping signal %d for tid %d after %u attempts\n",
+             sig, thread->unix_tid, SIGNAL_TARGET_RETRIES );
+    return 0;
+}
+
+#endif /* __OpenBSD__ */
