Files
darling-lol/patches/darlingserver.patch
T
David GhegheaandClaude Opus 5.5 c4135ab79f Raise darlingserver's soft fd limit when the hard limit is fixed
Under the rootless NixOS launcher darlingserver cannot raise its hard
RLIMIT_NOFILE to nr_open, so it kept the soft limit it inherited. Every
named memory entry holds a memfd in darlingserver, and its descriptors
peak at about 1100 when the League lobby opens. With an inherited soft
limit of 1024, memfd_create failed with EMFILE, mach_make_memory_entry_64
returned KERN_RESOURCE_SHORTAGE, and Chromium's CHECK in
PlatformSharedMemoryRegion::Create killed the League UX and the Riot
Client renderer after clicking Play.

When raising the hard limit fails, raise the soft limit to the inherited
hard limit instead. Log errno, size and fd limits when named-entry
creation fails, and log the live memfd count every 1024 creations so a
leak shows up in dserver.log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 22:45:23 +02:00

2958 lines
111 KiB
Diff

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4920879..012235e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -133,6 +133,13 @@ target_compile_options(darlingserver PRIVATE
-pthread
-std=c++17
)
+# The parent project commonly leaves CMAKE_BUILD_TYPE empty. An unoptimized
+# RPC server spends its event-loop budget in C++ ownership/container helpers.
+# Preserve explicit debug and sanitizer builds; optimize the default runtime.
+if (NOT CMAKE_BUILD_TYPE AND NOT DSERVER_ASAN AND NOT DSERVER_UBSAN)
+ target_compile_options(darlingserver PRIVATE -O2)
+endif()
+
target_link_options(darlingserver PRIVATE
-pthread
-Wl,--gc-sections
diff --git a/duct-tape/CMakeLists.txt b/duct-tape/CMakeLists.txt
index 5950c7f..cf4f1ba 100644
--- a/duct-tape/CMakeLists.txt
+++ b/duct-tape/CMakeLists.txt
@@ -268,6 +268,7 @@ add_library(darlingserver_duct_tape STATIC
src/kqchan.c
src/semaphore.c
src/psynch.c
+ src/fileport.c
src/condvar.c
src/debug.c
@@ -380,3 +381,9 @@ add_dependencies(darlingserver_duct_tape
target_link_libraries(darlingserver_duct_tape PUBLIC
libsimple_darlingserver
)
+
+# Match the host server's default optimization while retaining the kernel's
+# pointer-aliasing semantics and explicit debug/sanitizer build policies.
+if (NOT CMAKE_BUILD_TYPE AND NOT DSERVER_ASAN AND NOT DSERVER_UBSAN)
+ target_compile_options(darlingserver_duct_tape PRIVATE -O2 -fno-strict-aliasing)
+endif()
diff --git a/duct-tape/include/darlingserver/duct-tape.h b/duct-tape/include/darlingserver/duct-tape.h
index a5ddfc7..607cf17 100644
--- a/duct-tape/include/darlingserver/duct-tape.h
+++ b/duct-tape/include/darlingserver/duct-tape.h
@@ -56,6 +56,13 @@ dtape_thread_t* dtape_thread_create(dtape_task_t* task, uint64_t nsid, void* con
dtape_kqchan_mach_port_t* dtape_kqchan_mach_port_create(dtape_task_t* owning_task, uint32_t port, uint64_t receive_buffer, uint64_t receive_buffer_size, uint64_t saved_filter_flags, dtape_kqchan_mach_port_notification_callback_f notification_callback, void* context);
dtape_semaphore_t* dtape_semaphore_create(dtape_task_t* owning_task, int initial_value);
+/* darling-lol M54: fileports (Mojo FD-over-Mach transport). Names and
+ * kern_return_t cross as plain ints to keep this header light. */
+int dtape_fileport_makeport(dtape_task_t* task, int fd, uint32_t* name_out);
+int dtape_fileport_makefd(dtape_task_t* task, uint32_t name, int* fd_out);
+struct ipc_port;
+void dtape_fileport_destroy(struct ipc_port* port);
+
void dtape_kqchan_mach_port_destroy(dtape_kqchan_mach_port_t* kqchan);
void dtape_semaphore_destroy(dtape_semaphore_t* semaphore);
diff --git a/duct-tape/include/darlingserver/duct-tape/hooks.h b/duct-tape/include/darlingserver/duct-tape/hooks.h
index 1dde8b7..f319f1f 100644
--- a/duct-tape/include/darlingserver/duct-tape/hooks.h
+++ b/duct-tape/include/darlingserver/duct-tape/hooks.h
@@ -28,6 +28,8 @@ typedef dtape_thread_t* (*dtape_hook_current_thread_f)(void);
typedef void (*dtape_hook_timer_arm_f)(uint64_t absolute_ns, bool override);
typedef void (*dtape_hook_log_f)(dtape_log_level_t level, const char* message);
+/* darling-lol: lets dtape_log skip formatting (a 4 KiB vsnprintf per call) when the level would be dropped anyway */
+typedef int (*dtape_hook_log_enabled_f)(dtape_log_level_t level);
typedef void (*dtape_hook_get_load_info_f)(dtape_load_info_t* load_info);
typedef void (*dtape_hook_thread_suspend_f)(void* thread_context, dtape_thread_continuation_callback_f continuation_callback, void* continuation_contex, libsimple_lock_t* unlock_me);
@@ -122,6 +124,8 @@ typedef struct dtape_hooks {
dtape_hook_task_remove_port_set_member_f task_remove_port_set_member;
dtape_hook_task_clear_port_set_f task_clear_port_set;
#endif
+
+ dtape_hook_log_enabled_f log_enabled;
} dtape_hooks_t;
#ifdef __cplusplus
diff --git a/duct-tape/internal-include/darlingserver/duct-tape/stubs.h b/duct-tape/internal-include/darlingserver/duct-tape/stubs.h
index 32ebd76..688015e 100644
--- a/duct-tape/internal-include/darlingserver/duct-tape/stubs.h
+++ b/duct-tape/internal-include/darlingserver/duct-tape/stubs.h
@@ -12,9 +12,13 @@ void dtape_stub_log(const char* function_name, int safety, const char* subsectio
#define dtape_stub_safe(...) (dtape_stub_log(__FUNCTION__, 1, "" __VA_ARGS__))
// for functions that have been confirmed to require an actual implementation (rather than a simple stub)
-#define dtape_stub_unsafe(...) ({ \
- dtape_stub_log(__FUNCTION__, -1, "" __VA_ARGS__); \
- __builtin_unreachable(); \
- }) \
+//
+// darling-lol: this used to end in __builtin_unreachable(), which is only
+// valid when dtape_stub_log() is guaranteed to abort. With the optional
+// DTAPE_NONFATAL_UNSAFE escape hatch it may return, so we must not tell the
+// compiler the path is dead (that miscompiles the function's fall-through).
+// dtape_stub_log() raises SIGABRT itself in the default (fatal) case, so
+// omitting __builtin_unreachable() preserves the original behavior.
+#define dtape_stub_unsafe(...) (dtape_stub_log(__FUNCTION__, -1, "" __VA_ARGS__))
#endif // _DARLINGSERVER_DUCT_TAPE_STUBS_H_
diff --git a/duct-tape/internal-include/darlingserver/duct-tape/thread.h b/duct-tape/internal-include/darlingserver/duct-tape/thread.h
index 633d729..75a3965 100644
--- a/duct-tape/internal-include/darlingserver/duct-tape/thread.h
+++ b/duct-tape/internal-include/darlingserver/duct-tape/thread.h
@@ -38,6 +38,8 @@ struct dtape_thread {
bool processing_signal;
bool waiting_suspended;
+ /* Protect the wait check through publication of microthread suspension. */
+ libsimple_lock_t block_suspend_gate;
dtape_mutex_t suspension_mutex;
dtape_condvar_t suspension_condvar;
diff --git a/duct-tape/pthread/kern_synch.c b/duct-tape/pthread/kern_synch.c
index aa2b6d3..ab7cdb7 100644
--- a/duct-tape/pthread/kern_synch.c
+++ b/duct-tape/pthread/kern_synch.c
@@ -42,6 +42,7 @@
#include <sys/param.h>
#include <sys/queue.h>
#include <sys/resourcevar.h>
+#include <stdint.h> /* darling-lol M58f3: UINT64_MAX */
//#include <sys/proc_internal.h>
#include <sys/kauth.h>
#include <sys/systm.h>
@@ -1236,8 +1237,22 @@ _psynch_cvwait(__unused proc_t p, user_addr_t cv, uint64_t cvlsgen,
struct timespec ts;
ts.tv_sec = (__darwin_time_t)sec;
ts.tv_nsec = (nsec & 0x3fffffff);
- nanoseconds_to_absolutetime(
- (uint64_t)ts.tv_sec * NSEC_PER_SEC + ts.tv_nsec, &abstime);
+ /* darling-lol M58f3: saturate the seconds->nanoseconds product.
+ * Callers legitimately pass near-infinite deadlines (e.g.
+ * Chromium's base::WaitableEvent uses TimeDelta::Max(), so
+ * tv_sec can be ~9.2e12); the product overflows uint64 and
+ * wraps to an arbitrary (sometimes past!) deadline, making
+ * the wait return ETIMEDOUT instantly or hang indefinitely.
+ * Clamp to a huge-but-safe interval (UINT64_MAX/2 ns ~ 292
+ * years) which still cannot wrap after the deadline add. */
+ uint64_t nsecs;
+ uint64_t secs_u = (ts.tv_sec < 0) ? 0 : (uint64_t)ts.tv_sec;
+ if (secs_u > (UINT64_MAX / 2 - (uint64_t)ts.tv_nsec) / NSEC_PER_SEC) {
+ nsecs = UINT64_MAX / 2;
+ } else {
+ nsecs = secs_u * NSEC_PER_SEC + (uint64_t)ts.tv_nsec;
+ }
+ nanoseconds_to_absolutetime(nsecs, &abstime);
clock_absolutetime_interval_to_deadline(abstime, &abstime);
}
diff --git a/duct-tape/src/fileport.c b/duct-tape/src/fileport.c
new file mode 100644
index 0000000..1741d59
--- /dev/null
+++ b/duct-tape/src/fileport.c
@@ -0,0 +1,154 @@
+/*
+ * darling-lol M54: fileport support (fileport_makeport/fileport_makefd).
+ *
+ * Chromium's Mojo-on-Mac transports Unix FDs over Mach messages by wrapping
+ * them in fileports (mojo/core/channel_mac.cc, mojo/core/ipcz_driver/
+ * wrapped_platform_handle.cc). Without this, every FD-carrying Mojo message
+ * (e.g. NetworkService startup) dies in the old-LKM-API UD2 trap.
+ *
+ * A fileport is a Mach port whose kobject holds a host FD owned by
+ * darlingserver. The kobject rides the port across IPC transfer
+ * automatically, and ipc_kobject_destroy closes the FD when the port dies,
+ * so no transfer hooks or task-death cleanup are needed. fileport_makefd
+ * dups the held FD back to the caller (independent lifecycle, like XNU).
+ */
+
+#include <darlingserver/duct-tape.h>
+
+#include <kern/debug.h>
+#include <stdlib.h>
+#include <ipc/ipc_object.h>
+#include <ipc/ipc_port.h>
+#include <ipc/ipc_space.h>
+#include <ipc/ipc_entry.h>
+#include <kern/ipc_kobject.h>
+#include <mach/mach_port.h>
+#include <mach/message.h>
+
+/* duct-tape convention (see memory.c): declare libc bits manually rather
+ * than including host <unistd.h>, whose types collide with XNU headers. */
+int close(int fd);
+int dup(int fd);
+
+#include <darlingserver/duct-tape/task.h>
+#include <darlingserver/duct-tape/log.h>
+
+struct dtape_fileport {
+ int fd;
+};
+
+int dtape_fileport_makeport(dtape_task_t* task, int fd, uint32_t* name_out)
+{
+ struct dtape_fileport* fp;
+ ipc_port_t port;
+ mach_port_name_t name;
+
+ if (task == NULL || fd < 0 || name_out == NULL) {
+ return KERN_INVALID_ARGUMENT;
+ }
+ if (task->xnu_task.itk_space == IS_NULL) {
+ return KERN_INVALID_TASK;
+ }
+
+ fp = malloc(sizeof(*fp));
+ if (fp == NULL) {
+ dtape_log_error("fileport_makeport: malloc failed");
+ return KERN_RESOURCE_SHORTAGE;
+ }
+ fp->fd = fd;
+
+ /* Allocate a kernel-owned port holding the fd. As in XNU's
+ * fileport_alloc(), the port comes back with a send right. */
+ port = ipc_kobject_alloc_port((ipc_kobject_t)fp, IKOT_FILEPORT,
+ IPC_KOBJECT_ALLOC_MAKE_SEND | IPC_KOBJECT_ALLOC_NSREQUEST);
+ if (!IP_VALID(port)) {
+ dtape_log_error("fileport_makeport: ipc_kobject_alloc_port failed fd=%d", fd);
+ free(fp);
+ return KERN_RESOURCE_SHORTAGE;
+ }
+
+ /* Install the send right into the task's space and get a name.
+ * ipc_port_copyout_send() is the XNU entry point for exactly this
+ * (see sys_fileport_makeport); it consumes the send right, so we
+ * must not release it afterwards. Do NOT go through
+ * mach_port_insert_right(): its MACH_MSG_TYPE_PORT_ANY_RIGHT() guard
+ * rejects MACH_MSG_TYPE_MAKE_SEND, which made every fileport fail
+ * with KERN_INVALID_VALUE and broke all FD-carrying Mojo messages. */
+ name = ipc_port_copyout_send(port, task->xnu_task.itk_space);
+ if (!MACH_PORT_VALID(name)) {
+ dtape_log_error("fileport_makeport: ipc_port_copyout_send failed fd=%d", fd);
+ /* port consumed by copyout; its kobject destroy closes fp->fd */
+ return KERN_RESOURCE_SHORTAGE;
+ }
+
+ *name_out = (uint32_t)name;
+ return KERN_SUCCESS;
+}
+
+int dtape_fileport_makefd(dtape_task_t* task, uint32_t uname, int* fd_out)
+{
+ ipc_object_t object = IO_NULL;
+ mach_msg_type_name_t dtype = 0;
+ mach_port_name_t name = (mach_port_name_t)uname;
+ ipc_port_t port;
+ struct dtape_fileport* fp;
+ int fd;
+
+ if (task == NULL || fd_out == NULL) {
+ return KERN_INVALID_ARGUMENT;
+ }
+ if (task->xnu_task.itk_space == IS_NULL) {
+ return KERN_INVALID_TASK;
+ }
+
+ /* Copyin a send right: validates the name, checks aliveness, and
+ * enforces that the right is a send right (receive/dead names fail).
+ * Does not consume the caller's right. */
+ if (ipc_object_copyin(task->xnu_task.itk_space, name,
+ MACH_MSG_TYPE_COPY_SEND, &object, 0, NULL,
+ IPC_OBJECT_COPYIN_FLAGS_ALLOW_IMMOVABLE_SEND) != KERN_SUCCESS) {
+ return KERN_INVALID_RIGHT;
+ }
+ dtype = ipc_object_copyin_type(MACH_MSG_TYPE_COPY_SEND);
+
+ port = ip_object_to_port(object);
+ if (!IP_VALID(port) || ip_kotype(port) != IKOT_FILEPORT) {
+ ipc_object_destroy(object, dtype);
+ return KERN_INVALID_RIGHT;
+ }
+ fp = (struct dtape_fileport*)ip_get_kobject(port);
+ if (fp == NULL || fp->fd < 0) {
+ ipc_object_destroy(object, dtype);
+ return KERN_INVALID_RIGHT;
+ }
+
+ /* Independent lifecycle: the receiver owns the dup, the kobject
+ * keeps its own. */
+ fd = dup(fp->fd);
+ ipc_object_destroy(object, dtype);
+ if (fd < 0) {
+ return KERN_RESOURCE_SHORTAGE;
+ }
+
+ *fd_out = fd;
+ return KERN_SUCCESS;
+}
+
+void dtape_fileport_destroy(ipc_port_t port)
+{
+ struct dtape_fileport* fp;
+
+ if (!IP_VALID(port)) {
+ return;
+ }
+ fp = (struct dtape_fileport*)ip_get_kobject(port);
+ if (fp == NULL) {
+ return;
+ }
+ /* Port is dead; just release our resources like
+ * mach_destroy_memory_entry does. */
+ if (fp->fd >= 0) {
+ close(fp->fd);
+ }
+ free(fp);
+}
diff --git a/duct-tape/src/host.c b/duct-tape/src/host.c
index a841dfd..9fe8bb4 100644
--- a/duct-tape/src/host.c
+++ b/duct-tape/src/host.c
@@ -28,6 +28,64 @@ long sysconf(int name);
#define _SC_NPROCESSORS_CONF 83
#define _SC_NPROCESSORS_ONLN 84
+// Linux file access for the SMT topology read (manual prototypes, as for sysconf above)
+int open(const char* pathname, int flags, ...);
+long read(int fd, void* buf, unsigned long count);
+int close(int fd);
+
+// darling-lol: host_info used to report physical_cpu == logical_cpu, which
+// tells the guest there is no SMT. Read the sibling count of cpu0 from sysfs
+// and divide; on any failure keep the logical count. The per-CPU package/core
+// scan in the guest-side sysctl_hw.c is more precise, but host_info has no
+// other source for the physical count.
+static uint32_t host_smt_siblings(void) {
+ static uint32_t cached;
+ char buf[64];
+ uint32_t count = 0;
+ long n;
+
+ if (cached != 0) {
+ return cached;
+ }
+
+ int fd = open("/sys/devices/system/cpu/cpu0/topology/thread_siblings_list", 0 /* O_RDONLY */);
+ if (fd < 0) {
+ return 1;
+ }
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n <= 0) {
+ return 1;
+ }
+ buf[n] = 0;
+
+ // a list like "0,6" or "0-3": count one CPU per element, ranges as ranges
+ const char* s = buf;
+ while (*s >= '0' && *s <= '9') {
+ unsigned long first = 0, last;
+ while (*s >= '0' && *s <= '9') {
+ first = first * 10 + (unsigned long)(*s++ - '0');
+ }
+ last = first;
+ if (*s == '-') {
+ ++s;
+ last = 0;
+ while (*s >= '0' && *s <= '9') {
+ last = last * 10 + (unsigned long)(*s++ - '0');
+ }
+ }
+ if (last >= first && last - first < 4096) {
+ count += (uint32_t)(last - first + 1);
+ }
+ if (*s == ',') {
+ ++s;
+ }
+ }
+
+ cached = (count != 0) ? count : 1;
+ return cached;
+}
+
static void cache_sysinfo(void* context) {
struct sysinfo* cached_sysinfo = context;
@@ -73,11 +131,16 @@ kern_return_t host_info(host_t host, host_flavor_t flavor, host_info_t info, mac
// if there's room for the modern structure, fill in some additional info
if (*count >= HOST_BASIC_INFO_COUNT) {
- // TODO: properly differentiate physical vs. logical cores
+ uint32_t siblings = host_smt_siblings();
dtape_stub_safe("modern HOST_BASIC_INFO");
basic_info->cpu_threadtype = CPU_THREADTYPE_NONE;
- basic_info->physical_cpu = basic_info->avail_cpus;
- basic_info->physical_cpu_max = basic_info->max_cpus;
+ // darling-lol: physical cores, not logical CPUs (the guest
+ // sizes worker pools off physical_cpu and libmalloc derives
+ // its SMT sharing from the commpage's physical/logical pair).
+ basic_info->physical_cpu = (siblings != 0 && basic_info->avail_cpus % siblings == 0)
+ ? basic_info->avail_cpus / siblings : basic_info->avail_cpus;
+ basic_info->physical_cpu_max = (siblings != 0 && basic_info->max_cpus % siblings == 0)
+ ? basic_info->max_cpus / siblings : basic_info->max_cpus;
basic_info->logical_cpu = basic_info->avail_cpus;
basic_info->logical_cpu_max = basic_info->max_cpus;
diff --git a/duct-tape/src/kqchan.c b/duct-tape/src/kqchan.c
index 458e659..2e33f20 100644
--- a/duct-tape/src/kqchan.c
+++ b/duct-tape/src/kqchan.c
@@ -138,7 +138,16 @@ static void knote_post(struct knote* kn, long hint) {
dtape_log_debug("%s: kn=%p, kqchan=%p, kqchan->callback=%p", __FUNCTION__, kn, kqchan, kqchan->callback);
- if (!kqchan->callback) {
+ /* darling-lol: snapshot callback+context with a SINGLE load.
+ * The check-then-call used to load kqchan->callback twice; with -O0
+ * builds there is no CSE, so dtape_kqchan_mach_port_disable_notifications
+ * (or destroy on another worker) could NULL it between the guard and
+ * the call, producing call-0x0 -> SIGSEGV in darlingserver whenever a
+ * port with knotes was torn down (e.g. GPU helpers exiting). A single
+ * snapshot cannot call NULL. */
+ dtape_kqchan_mach_port_notification_callback_f cb = kqchan->callback;
+ void* ctx = kqchan->context;
+ if (!cb) {
return;
}
@@ -150,7 +159,7 @@ static void knote_post(struct knote* kn, long hint) {
dtape_log_debug("%s: kn=%p, kqchan=%p; invoking callback...", __FUNCTION__, kn, kqchan);
- kqchan->callback(kqchan->context);
+ cb(ctx);
};
void knote(struct klist* list, long hint) {
@@ -193,7 +202,12 @@ static void kqchan_waitq_waiter_entry(void* context, wait_result_t wait_result)
break;
} else {
if (filt_machportpeek(&kqchan->knote) & FILTER_ACTIVE) {
- kqchan->callback(kqchan->context);
+ /* darling-lol: single-load snapshot, see knote_post. */
+ dtape_kqchan_mach_port_notification_callback_f wcb = kqchan->callback;
+ void* wctx = kqchan->context;
+ if (wcb) {
+ wcb(wctx);
+ }
}
// wait until it's read
if (!dtape_semaphore_down_simple(kqchan->waiter_read_semaphore)) {
diff --git a/duct-tape/src/memory.c b/duct-tape/src/memory.c
index 3114947..20cd162 100644
--- a/duct-tape/src/memory.c
+++ b/duct-tape/src/memory.c
@@ -11,6 +11,7 @@
#include <stdlib.h>
+
#include <mach_debug/mach_debug.h>
struct zone {
@@ -52,6 +53,26 @@ int ftruncate(int fd, off_t length);
#define MFD_CLOEXEC 0x1
+/* darling-lol diagnostics: why shared-memory creation fails. */
+int* __errno_location(void);
+struct dtape_diag_rlimit { unsigned long cur, max; };
+int prlimit(int pid, int resource, const struct dtape_diag_rlimit* new_limit, struct dtape_diag_rlimit* old_limit);
+#define DTAPE_DIAG_RLIMIT_NOFILE 7
+
+static long dtape_diag_live_descriptors;
+static unsigned long dtape_diag_created_descriptors;
+
+static void dtape_diag_log_shortage(const char* what, int fd, uint64_t size) {
+ int err = *__errno_location();
+ struct dtape_diag_rlimit limit = {0, 0};
+ prlimit(0, DTAPE_DIAG_RLIMIT_NOFILE, NULL, &limit);
+ dtape_log_error("MEMDIAG %s failed errno=%d size=%#llx fd=%d live=%ld created=%lu nofile=%lu/%lu",
+ what, err, (unsigned long long)size, fd,
+ __atomic_load_n(&dtape_diag_live_descriptors, __ATOMIC_RELAXED),
+ __atomic_load_n(&dtape_diag_created_descriptors, __ATOMIC_RELAXED),
+ limit.cur, limit.max);
+}
+
void dtape_memory_init(void) {
};
@@ -107,6 +128,12 @@ static dtape_map_shared_descriptor_t* dtape_map_shared_descriptor_create(int mem
desc->memfd = memfd;
desc->size = size;
+ long live = __atomic_add_fetch(&dtape_diag_live_descriptors, 1, __ATOMIC_RELAXED);
+ unsigned long created = __atomic_add_fetch(&dtape_diag_created_descriptors, 1, __ATOMIC_RELAXED);
+ if (created % 1024 == 0) {
+ dtape_log_error("MEMDIAG descriptors live=%ld created=%lu last_fd=%d", live, created, memfd);
+ }
+
return desc;
};
@@ -121,6 +148,7 @@ static void dtape_map_shared_descriptor_release(dtape_map_shared_descriptor_t* d
close(desc->memfd);
free(desc);
+ __atomic_sub_fetch(&dtape_diag_live_descriptors, 1, __ATOMIC_RELAXED);
};
static dtape_map_shared_entry_t* dtape_map_shared_entry_create(uint64_t address, uint64_t size, uint64_t page_offset, dtape_map_shared_descriptor_t* descriptor) {
@@ -341,7 +369,14 @@ kern_return_t copyoutmap(vm_map_t map, void* fromdata, vm_map_address_t toaddr,
memmove((void*)toaddr, fromdata, length);
return KERN_SUCCESS;
} else {
- return dtape_hooks->task_write_memory(map->dtape_task->context, toaddr, fromdata, length) ? KERN_SUCCESS : KERN_FAILURE;
+ /* darling-lol M58dq: name the failing user-copy target behind the M58G
+ * ipc_kmsg_put "copyout failed" drops. */
+ if (!dtape_hooks->task_write_memory(map->dtape_task->context, toaddr, fromdata, length)) {
+ dtape_log_error("M58DQ copyoutmap FAIL toaddr=0x%llx len=%lu map=%p task=%p",
+ (unsigned long long) toaddr, (unsigned long) length, (void*) map, (void*) map->dtape_task);
+ return KERN_FAILURE;
+ }
+ return KERN_SUCCESS;
}
};
@@ -526,20 +561,218 @@ kern_return_t mach_vm_allocate_kernel(vm_map_t map, mach_vm_offset_t* addr, mach
}
};
+/*
+ * darling-lol: functional `_mach_make_memory_entry`.
+ *
+ * This used to be `dtape_stub_unsafe()` (abort), so any guest call killed
+ * darlingserver outright. Entries are backed by a SHARED memfd held in a
+ * refcounted descriptor on the entry: every task mapping the entry observes
+ * the same pages (required e.g. for Chromium field-trial shared memory,
+ * where the browser writes after creating and helpers map later).
+ */
+kern_return_t mach_memory_entry_allocate(vm_named_entry_t* user_entry_p, ipc_port_t* user_handle_p) {
+ vm_named_entry_t user_entry;
+ ipc_port_t user_handle;
+
+ user_entry = (vm_named_entry_t) kalloc(sizeof(*user_entry));
+ if (user_entry == NULL) {
+ return KERN_FAILURE;
+ }
+ bzero(user_entry, sizeof(*user_entry));
+
+ named_entry_lock_init(user_entry);
+ user_entry->backing.copy = NULL;
+ user_entry->is_object = FALSE;
+ user_entry->is_sub_map = FALSE;
+ user_entry->is_copy = FALSE;
+ user_entry->internal = FALSE;
+ user_entry->size = 0;
+ user_entry->offset = 0;
+ user_entry->data_offset = 0;
+ user_entry->protection = VM_PROT_NONE;
+ user_entry->ref_count = 1;
+
+ user_handle = ipc_kobject_alloc_port((ipc_kobject_t)user_entry,
+ IKOT_NAMED_ENTRY,
+ IPC_KOBJECT_ALLOC_MAKE_SEND | IPC_KOBJECT_ALLOC_NSREQUEST);
+ if (user_handle == IP_NULL) {
+ kfree(user_entry, sizeof(*user_entry));
+ return KERN_FAILURE;
+ }
+
+ *user_entry_p = user_entry;
+ *user_handle_p = user_handle;
+ return KERN_SUCCESS;
+};
+
kern_return_t _mach_make_memory_entry(vm_map_t target_map, memory_object_size_t* size, memory_object_offset_t offset, vm_prot_t permission, ipc_port_t* object_handle, ipc_port_t parent_entry) {
- dtape_stub_unsafe();
+ vm_map_size_t map_size = (vm_map_size_t)*size;
+ vm_map_offset_t map_start, map_end;
+ vm_map_copy_t copy = NULL;
+ vm_named_entry_t user_entry = NULL;
+ ipc_port_t user_handle = IP_NULL;
+ kern_return_t kr;
+
+ if (object_handle == NULL) {
+ return KERN_INVALID_ARGUMENT;
+ }
+ *object_handle = IP_NULL;
+
+ if (target_map == VM_MAP_NULL) {
+ target_map = current_map();
+ }
+
+ /* round the request out to whole pages, like XNU does */
+ map_start = vm_map_trunc_page(offset, VM_MAP_PAGE_MASK(target_map));
+ map_end = vm_map_round_page(offset + map_size, VM_MAP_PAGE_MASK(target_map));
+ map_size = map_end - map_start;
+
+ if (map_size == 0) {
+ return KERN_INVALID_ARGUMENT;
+ }
+
+ /*
+ * Backing strategy (darling-lol): a SHARED memfd, so every task that
+ * maps the entry sees the same pages. The previous snapshot-copy
+ * approach handed each mapper a private copy of the bytes as of
+ * creation time; a producer (e.g. Chromium's browser writing field
+ * trial state) then wrote into pages no consumer ever saw, and
+ * consumers parsed stale zeros -> garbage strings -> crashes
+ * (char_traits overlap assert) in every helper.
+ *
+ * Two cases, matching XNU:
+ *
+ * - MAP_MEM_NAMED_CREATE: fresh anonymous entry of *size bytes (what
+ * Chromium's PlatformSharedMemoryRegion allocation path wants).
+ * memfds start zero-filled. A null parent alone does NOT imply this:
+ * ordinary extraction calls also pass a null parent.
+ *
+ * - parent_entry != NULL: seed the memfd with a snapshot of the
+ * described region (copied once, at creation).
+ */
+ int memfd = -1;
+ dtape_map_shared_descriptor_t* desc = NULL;
+ if (permission & MAP_MEM_NAMED_CREATE) {
+ memfd = memfd_create("darling-mementry", MFD_CLOEXEC);
+ if (memfd < 0) {
+ dtape_diag_log_shortage("named-create memfd_create", memfd, map_size);
+ return KERN_RESOURCE_SHORTAGE;
+ }
+ if (ftruncate(memfd, map_size) < 0) {
+ dtape_diag_log_shortage("named-create ftruncate", memfd, map_size);
+ close(memfd);
+ return KERN_RESOURCE_SHORTAGE;
+ }
+ } else {
+ void* staged = NULL;
+ int source_prot = PROT_READ | PROT_WRITE;
+ uintptr_t remapped;
+ /* snapshot the region, then publish it into the shared object */
+ kr = vm_map_copyin_common(target_map, map_start, map_size,
+ FALSE /* src_destroy */, FALSE /* src_volatile */, &copy,
+ FALSE /* use_maxprot */);
+ if (kr != KERN_SUCCESS) {
+ dtape_log_error("mach_make_memory_entry: copyin failed kr=%d map=%p start=%#llx size=%#llx perms=%#x",
+ kr, (void*)target_map, (unsigned long long)map_start, (unsigned long long)map_size, permission);
+ return kr;
+ }
+ memfd = memfd_create("darling-mementry", MFD_CLOEXEC);
+ if (memfd < 0 || ftruncate(memfd, map_size) < 0) {
+ if (memfd >= 0) close(memfd);
+ vm_map_copy_discard(copy);
+ return KERN_RESOURCE_SHORTAGE;
+ }
+ staged = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, memfd, 0);
+ if (staged == MAP_FAILED) {
+ close(memfd);
+ vm_map_copy_discard(copy);
+ return KERN_RESOURCE_SHORTAGE;
+ }
+ memcpy(staged, (char*)copy->cpy_kdata + copy->offset, map_size);
+ munmap(staged, map_size);
+ vm_map_copy_discard(copy);
+ copy = NULL;
+
+ /* Extraction must retain shared-object semantics, not snapshot
+ * semantics. Replace the source task's pages with a MAP_SHARED view
+ * of the populated memfd. Future writes by the creator are then
+ * visible to every process that maps the named entry. */
+ /* The requested permission constrains mappings made from the named
+ * entry; it does not downgrade the creator's existing mapping. The
+ * common Chromium extraction path exports a read-only handle from a
+ * writable region and continues writing through the original view. */
+ if (permission & VM_PROT_EXECUTE)
+ source_prot |= PROT_EXEC;
+ remapped = dtape_hooks->task_map_file(
+ target_map->dtape_task->context, memfd,
+ dtape_byte_count_to_page_count_round_up(map_size), source_prot,
+ map_start, 0,
+ dtape_memory_flag_fixed | dtape_memory_flag_overwrite);
+ if (remapped != map_start) {
+ close(memfd);
+ return KERN_FAILURE;
+ }
+ }
+
+ desc = dtape_map_shared_descriptor_create(memfd, map_size);
+ if (desc == NULL) {
+ dtape_diag_log_shortage("named-entry descriptor malloc", memfd, map_size);
+ close(memfd);
+ return KERN_RESOURCE_SHORTAGE;
+ }
+ memfd = -1; /* the descriptor now owns the memfd */
+
+ kr = mach_memory_entry_allocate(&user_entry, &user_handle);
+ if (kr != KERN_SUCCESS) {
+ dtape_map_shared_descriptor_release(desc);
+ dtape_log_error("mach_make_memory_entry: allocate failed kr=%d", kr);
+ return kr;
+ }
+
+ named_entry_lock(user_entry);
+ user_entry->backing.copy = NULL;
+ user_entry->is_copy = FALSE;
+ user_entry->dtape_descriptor = desc;
+ user_entry->size = map_size;
+ user_entry->offset = offset - map_start; /* logical offset, memfd base 0 */
+ user_entry->data_offset = 0;
+ user_entry->protection = permission;
+ named_entry_unlock(user_entry);
+
+ *size = (memory_object_size_t)map_size;
+ *object_handle = user_handle;
+ return KERN_SUCCESS;
+};
+
+kern_return_t mach_make_memory_entry_64(vm_map_t target_map, memory_object_size_t* size, memory_object_offset_t offset, vm_prot_t permission, ipc_port_t* object_handle, ipc_port_t parent_handle) {
+ return _mach_make_memory_entry(target_map, size, offset, permission, object_handle, parent_handle);
+};
+
+kern_return_t mach_make_memory_entry(vm_map_t target_map, vm_size_t* size, vm_offset_t offset, vm_prot_t permission, ipc_port_t* object_handle, ipc_port_t parent_entry) {
+ memory_object_size_t mo_size = (memory_object_size_t)*size;
+ kern_return_t kr = _mach_make_memory_entry(target_map, &mo_size, (memory_object_offset_t)offset, permission, object_handle, parent_entry);
+ *size = (vm_size_t)mo_size;
+ return kr;
};
kern_return_t mach_memory_entry_access_tracking(ipc_port_t entry_port, int* access_tracking, uint32_t* access_tracking_reads, uint32_t* access_tracking_writes) {
- dtape_stub_unsafe();
+ /* No access tracking in duct-tape; report "no tracking" rather than
+ * aborting the server. */
+ if (access_tracking) *access_tracking = 0;
+ if (access_tracking_reads) *access_tracking_reads = 0;
+ if (access_tracking_writes) *access_tracking_writes = 0;
+ return KERN_SUCCESS;
};
kern_return_t mach_memory_entry_ownership(ipc_port_t entry_port, task_t owner, int ledger_tag, int ledger_flags) {
- dtape_stub_unsafe();
+ /* Ledger ownership is not modeled; accept and ignore. */
+ return KERN_SUCCESS;
};
kern_return_t mach_memory_entry_purgable_control(ipc_port_t entry_port, vm_purgable_t control, int* state) {
- dtape_stub_unsafe();
+ /* Not purgable here. */
+ if (state) *state = VM_PURGABLE_NONVOLATILE;
+ return KERN_SUCCESS;
};
kern_return_t mach_memory_info(host_priv_t host, mach_zone_name_array_t* namesp, mach_msg_type_number_t* namesCntp, mach_zone_info_array_t* infop, mach_msg_type_number_t* infoCntp, mach_memory_info_array_t* memoryInfop, mach_msg_type_number_t* memoryInfoCntp) {
@@ -591,8 +824,115 @@ kern_return_t mach_vm_machine_attribute(vm_map_t map, mach_vm_address_t addr, ma
};
kern_return_t mach_vm_map_external(vm_map_t target_map, mach_vm_offset_t* address, mach_vm_size_t initial_size, mach_vm_offset_t mask, int flags, ipc_port_t port, vm_object_offset_t offset, boolean_t copy, vm_prot_t cur_protection, vm_prot_t max_protection, vm_inherit_t inheritance) {
- dtape_stub_safe();
- return KERN_SUCCESS;
+ /*
+ * darling-lol: real implementation. This used to be a "safe" stub
+ * returning KERN_SUCCESS without mapping anything or setting
+ * *address -- so every VM_FLAGS_ANYWHERE caller (notably libplatform's
+ * os_alloc heap via mach_vm_map, which backs ALL os_alloc_once
+ * singletons incl. libnotify globals) got success plus a stale address
+ * and crashed later on the unmapped pointer. Two cases:
+ *
+ * - MEMORY_OBJECT_NULL port: anonymous mapping. Allocate fresh pages
+ * in the target task (zero-filled by the host mmap underneath).
+ * - named-entry port (from _mach_make_memory_entry): map a duplicate
+ * of the entry's kernel-buffer copy at the requested offset. The
+ * entry keeps its own copy so it stays mappable (and destroyable)
+ * afterwards. NOTE: this gives copy semantics, not true cross-task
+ * sharing; sufficient to unblock, revisit if sharing artifacts show.
+ */
+ int prot = 0;
+ if (cur_protection & VM_PROT_READ)
+ prot |= PROT_READ;
+ if (cur_protection & VM_PROT_WRITE)
+ prot |= PROT_WRITE;
+ if (cur_protection & VM_PROT_EXECUTE)
+ prot |= PROT_EXEC;
+
+ if (target_map == VM_MAP_NULL) {
+ target_map = current_map();
+ }
+ if (target_map == VM_MAP_NULL || address == NULL) {
+ return KERN_INVALID_ARGUMENT;
+ }
+
+ if (port == MEMORY_OBJECT_NULL) {
+ uintptr_t hint = (flags & VM_FLAGS_ANYWHERE) ? 0 : (uintptr_t)*address;
+ uintptr_t addr = dtape_hooks->task_allocate_pages(
+ target_map->dtape_task->context,
+ dtape_byte_count_to_page_count_round_up(initial_size),
+ prot, hint, 0);
+ if (addr == 0) {
+ return KERN_RESOURCE_SHORTAGE;
+ }
+ *address = (mach_vm_offset_t)addr;
+ return KERN_SUCCESS;
+ }
+
+ if (IP_VALID(port) && ip_kotype(port) == IKOT_NAMED_ENTRY) {
+ vm_named_entry_t entry = (vm_named_entry_t)ip_get_kobject(port);
+ dtape_map_shared_descriptor_t* desc = NULL;
+ vm_map_offset_t entry_off = 0;
+ vm_map_size_t entry_size = 0;
+ vm_prot_t entry_prot = VM_PROT_NONE;
+ uintptr_t map_addr;
+ uint64_t page_off;
+
+ named_entry_lock(entry);
+ desc = (dtape_map_shared_descriptor_t*)entry->dtape_descriptor;
+ entry_off = entry->offset;
+ entry_size = entry->size;
+ entry_prot = entry->protection;
+ if (desc != NULL) {
+ dtape_map_shared_descriptor_retain(desc);
+ }
+ named_entry_unlock(entry);
+
+ if (desc == NULL) {
+ return KERN_INVALID_ARGUMENT;
+ }
+ /* darling-lol M53: enforce the entry's max protection. XNU
+ * refuses a writable mapping of a read-only entry with
+ * KERN_INVALID_RIGHT, and Chromium's shared-memory Take()
+ * depends on exactly that signal to detect read-only regions
+ * (without it, every readonly Mojo SHM probe "succeeds"
+ * writable and the GPU dies in a CHECK). Mask to VM_PROT bits:
+ * creation permission carries MAP_MEM_NAMED_CREATE etc. */
+ if ((cur_protection & (VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE) &
+ ~(entry_prot & (VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE))) != 0) {
+ dtape_map_shared_descriptor_release(desc);
+ return KERN_INVALID_RIGHT;
+ }
+ if ((vm_map_offset_t)offset >= entry_size) {
+ dtape_map_shared_descriptor_release(desc);
+ return KERN_INVALID_ARGUMENT;
+ }
+ /* Map the shared object itself (MAP_SHARED in the target), so
+ * all tasks mapping the entry observe the same pages. */
+ page_off = ((uint64_t)entry_off + (uint64_t)offset) / 4096;
+ if ((flags & VM_FLAGS_ANYWHERE) != 0) {
+ map_addr = dtape_hooks->task_map_file(
+ target_map->dtape_task->context,
+ desc->memfd,
+ dtape_byte_count_to_page_count_round_up(initial_size),
+ prot, 0, page_off, dtape_memory_flag_none);
+ } else {
+ map_addr = dtape_hooks->task_map_file(
+ target_map->dtape_task->context,
+ desc->memfd,
+ dtape_byte_count_to_page_count_round_up(initial_size),
+ prot, (uintptr_t)*address, page_off,
+ dtape_memory_flag_fixed | dtape_memory_flag_overwrite);
+ }
+ dtape_map_shared_descriptor_release(desc);
+ if (map_addr == 0) {
+ return KERN_FAILURE;
+ }
+ *address = (mach_vm_offset_t)map_addr +
+ (((uint64_t)entry_off + (uint64_t)offset) % 4096);
+ return KERN_SUCCESS;
+ }
+
+ return KERN_INVALID_ARGUMENT;
};
kern_return_t mach_vm_msync(vm_map_t map, mach_vm_offset_t address, mach_vm_size_t size, vm_sync_t sync_flags) {
@@ -620,15 +960,22 @@ kern_return_t mach_vm_msync(vm_map_t map, mach_vm_offset_t address, mach_vm_size
};
kern_return_t mach_vm_page_info(vm_map_t map, mach_vm_address_t address, vm_page_info_flavor_t flavor, vm_page_info_t info, mach_msg_type_number_t* count) {
- dtape_stub_unsafe();
+ // Page accounting is not implemented; let the caller handle unsupported queries.
+ if (count) *count = 0;
+ return KERN_NOT_SUPPORTED;
};
kern_return_t mach_vm_page_query(vm_map_t map, mach_vm_offset_t offset, int* disposition, int* ref_count) {
- dtape_stub_unsafe();
+ // Page accounting is not implemented; let the caller handle unsupported queries.
+ if (disposition) *disposition = 0;
+ if (ref_count) *ref_count = 0;
+ return KERN_NOT_SUPPORTED;
};
kern_return_t mach_vm_page_range_query(vm_map_t map, mach_vm_offset_t address, mach_vm_size_t size, mach_vm_address_t dispositions_addr, mach_vm_size_t* dispositions_count) {
- dtape_stub_unsafe();
+ // Page accounting is not implemented; let the caller handle unsupported queries.
+ if (dispositions_count) *dispositions_count = 0;
+ return KERN_NOT_SUPPORTED;
};
kern_return_t mach_vm_protect(vm_map_t map, mach_vm_offset_t start, mach_vm_size_t size, boolean_t set_maximum, vm_prot_t new_protection) {
@@ -756,7 +1103,8 @@ region_info_out:
return kr;
};
default:
- dtape_stub_unsafe("Unimplemented flavor");
+ if (object_name) *object_name = IP_NULL;
+ return KERN_INVALID_ARGUMENT;
}
};
@@ -856,7 +1204,7 @@ static kern_return_t mach_vm_remap_external_shared(vm_map_t target_map, mach_vm_
src_existing_entry_count = dtape_map_find_shared_entries_locked(src_map, start_memaddr, map_size, src_existing_entries, sizeof(src_existing_entries) / sizeof(*src_existing_entries));
- if (src_existing_entry_count == 1 && src_existing_entries[0]->address == start_memaddr && src_existing_entries[0]->size >= map_size) {
+ if (src_existing_entry_count == 1 && src_existing_entries[0]->address == start_memaddr && src_existing_entries[0]->size == map_size) {
// special case for what LLDB does with dyld info
dtape_map_shared_descriptor_retain(src_existing_entries[0]->descriptor);
descriptor = src_existing_entries[0]->descriptor;
@@ -1441,6 +1789,10 @@ mach_destroy_memory_entry(
named_entry->ref_count -= 1;
if (named_entry->ref_count == 0) {
+ /* darling-lol: release duct-tape shared backing first. */
+ dtape_map_shared_descriptor_t* desc =
+ (dtape_map_shared_descriptor_t*)named_entry->dtape_descriptor;
+ named_entry->dtape_descriptor = NULL;
if (named_entry->is_sub_map) {
vm_map_deallocate(named_entry->backing.map);
} else if (named_entry->is_copy) {
@@ -1456,6 +1808,9 @@ mach_destroy_memory_entry(
named_entry_unlock(named_entry);
named_entry_lock_destroy(named_entry);
+ if (desc != NULL) {
+ dtape_map_shared_descriptor_release(desc);
+ }
#if VM_NAMED_ENTRY_LIST
lck_mtx_lock_spin(&vm_named_entry_list_lock_data);
diff --git a/duct-tape/src/misc.c b/duct-tape/src/misc.c
index afe60fc..02968b2 100644
--- a/duct-tape/src/misc.c
+++ b/duct-tape/src/misc.c
@@ -48,6 +48,9 @@ ssize_t getrandom(void* buf, size_t buflen, unsigned int flags);
void dtape_logv(dtape_log_level_t level, const char* format, va_list args) {
+ if (dtape_hooks->log_enabled && !dtape_hooks->log_enabled(level)) {
+ return;
+ }
char message[4096];
vsnprintf(message, sizeof(message), format, args);
dtape_hooks->log(level, message);
@@ -87,10 +90,28 @@ int scnprintf(char* buffer, size_t buffer_size, const char* format, ...) {
void (ipc_kmsg_trace_send)(ipc_kmsg_t kmsg, mach_msg_option_t option) {
pid_t dest_pid = -1;
ipc_port_t dest = kmsg->ikm_header->msgh_remote_port;
+ mach_msg_header_t* h = kmsg->ikm_header;
dest_pid = ipc_port_get_receiver_task(dest, NULL);
- dtape_log_debug("sending kmsg %p to pid %d", kmsg, dest_pid);
+ /* darling-lol M58 IPC probe: log enough of the Mach message to identify
+ * the Chromium/Mojo request or reply. Mojo's ChannelMac puts the Mojo
+ * message (with its 4-byte interface ordinal) in the Mach payload; the
+ * offset depends on the message layout, so dump a few words. */
+ {
+ unsigned char* p = (unsigned char*)h;
+ unsigned int size = h->msgh_size;
+ unsigned int w88 = 0, w92 = 0, w96 = 0, w100 = 0, w104 = 0, w108 = 0;
+ if (size >= 92) w88 = *(unsigned int*)(p + 88);
+ if (size >= 96) w92 = *(unsigned int*)(p + 92);
+ if (size >= 100) w96 = *(unsigned int*)(p + 96);
+ if (size >= 104) w100 = *(unsigned int*)(p + 100);
+ if (size >= 108) w104 = *(unsigned int*)(p + 104);
+ if (size >= 112) w108 = *(unsigned int*)(p + 108);
+ dtape_log_debug("sending kmsg to pid %d id=%#x size=%u bits=%#x b88=%08x b92=%08x b96=%08x b100=%08x b104=%08x b108=%08x",
+ dest_pid, h->msgh_id, size, h->msgh_bits,
+ w88, w92, w96, w100, w104, w108);
+ }
};
void Assert(const char* file, int line, const char* expression) {
diff --git a/duct-tape/src/stubs.c b/duct-tape/src/stubs.c
index dc1cfa5..39a8c8d 100644
--- a/duct-tape/src/stubs.c
+++ b/duct-tape/src/stubs.c
@@ -37,9 +37,18 @@ extern char* getenv(const char* name);
static bool log_safe_stubs = false;
+static bool nonfatal_unsafe_stubs = false;
+
__attribute__((constructor))
static void init_stubs() {
log_safe_stubs = getenv("DTAPE_LOG_SAFE_STUBS") != NULL;
+ /* darling-lol: optional escape hatch. When set, an "unsafe" (unimplemented,
+ * needs-real-impl) duct-tape stub logs but does NOT abort darlingserver.
+ * Aborting the server is catastrophic: it is a single process serving every
+ * guest process, so one unimplemented trap (e.g. a Chromium mach call) tears
+ * down the whole container's RPC, and every guest then fails with -111.
+ * Default remains fatal so genuine bugs stay loud during development. */
+ nonfatal_unsafe_stubs = getenv("DTAPE_NONFATAL_UNSAFE") != NULL;
};
void dtape_stub_log(const char* function_name, int safety, const char* subsection) {
@@ -57,7 +66,7 @@ void dtape_stub_log(const char* function_name, int safety, const char* subsectio
kind_info = "";
} else if (safety < 0) {
log_level = dtape_log_level_error;
- do_abort = true;
+ do_abort = !nonfatal_unsafe_stubs;
kind_info = " (unsafe)";
} else {
if (!log_safe_stubs) {
@@ -210,11 +219,15 @@ kern_return_t kmod_get_info(host_t host, kmod_info_array_t* kmod_list, mach_msg_
};
kern_return_t kext_request(host_priv_t hostPriv, uint32_t clientLogSpec, vm_offset_t requestIn, mach_msg_type_number_t requestLengthIn, vm_offset_t* responseOut, mach_msg_type_number_t* responseLengthOut, vm_offset_t* logDataOut, mach_msg_type_number_t* logDataLengthOut, kern_return_t* op_result) {
- dtape_stub_unsafe();
+ // there are no kernel extensions to query or load; fail the request
+ dtape_stub_safe();
+ return KERN_NOT_SUPPORTED;
};
uint32_t PE_i_can_has_debugger(uint32_t* something) {
- dtape_stub_unsafe();
+ // No kernel debugger capability is provided by this runtime.
+ if (something) *something = 0;
+ return 0;
};
bool work_interval_port_type_render_server(mach_port_name_t port_name) {
diff --git a/duct-tape/src/task.c b/duct-tape/src/task.c
index a9eb4bc..7ef2800 100644
--- a/duct-tape/src/task.c
+++ b/duct-tape/src/task.c
@@ -521,7 +521,9 @@ kern_return_t task_info(task_t xtask, task_flavor_t flavor, task_info_t task_inf
};
default:
- dtape_stub_unsafe("unimplemented flavor");
+ /* Unsupported task-info flavors are guest API errors, not fatal
+ * host errors. Chromium probes optional power/accounting data. */
+ return KERN_INVALID_ARGUMENT;
}
};
@@ -585,7 +587,7 @@ kern_return_t task_restartable_ranges_synchronize(task_t task) {
};
kern_return_t task_set_exc_guard_behavior(task_t task, task_exc_guard_behavior_t behavior) {
- dtape_stub_unsafe();
+ return KERN_NOT_SUPPORTED;
};
kern_return_t task_set_info(task_t task, task_flavor_t flavor, task_info_t task_info_in, mach_msg_type_number_t task_info_count) {
@@ -593,7 +595,8 @@ kern_return_t task_set_info(task_t task, task_flavor_t flavor, task_info_t task_
};
kern_return_t task_set_phys_footprint_limit(task_t task, int new_limit_mb, int* old_limit_mb) {
- dtape_stub_unsafe();
+ if (old_limit_mb) *old_limit_mb = 0;
+ return KERN_NOT_SUPPORTED;
};
kern_return_t task_set_state(task_t task, int flavor, thread_state_t state, mach_msg_type_number_t state_count) {
diff --git a/duct-tape/src/thread.c b/duct-tape/src/thread.c
index f4ce909..a6cdb2c 100644
--- a/duct-tape/src/thread.c
+++ b/duct-tape/src/thread.c
@@ -53,6 +53,7 @@ dtape_thread_t* dtape_thread_create(dtape_task_t* task, uint64_t nsid, void* con
thread->processing_signal = false;
thread->name = NULL;
thread->waiting_suspended = false;
+ libsimple_lock_init(&thread->block_suspend_gate);
LIST_INIT(&thread->user_states);
dtape_mutex_init(&thread->suspension_mutex);
dtape_condvar_init(&thread->suspension_condvar);
@@ -521,11 +522,16 @@ wait_result_t thread_block_parameter(thread_continue_t continuation, void* param
thread->xnu_thread.parameter = parameter;
bool waiting = thread->xnu_thread.state & TH_WAIT;
+ /* A wakeup between this check and Thread::suspend used to be ignored
+ * because the microthread was not suspended yet. Keep the gate locked
+ * until the server has yielded and published the suspended state. */
+ if (waiting)
+ libsimple_lock_lock(&thread->block_suspend_gate);
thread_unlock(&thread->xnu_thread);
if (waiting) {
- dtape_hooks->thread_suspend(thread->context, continuation ? thread_continuation_callback : NULL, thread, NULL);
+ dtape_hooks->thread_suspend(thread->context, continuation ? thread_continuation_callback : NULL, thread, &thread->block_suspend_gate);
}
thread_lock(&thread->xnu_thread);
@@ -548,8 +554,20 @@ wait_result_t thread_block(thread_continue_t continuation) {
// thread locked
boolean_t thread_unblock(thread_t xthread, wait_result_t wresult) {
dtape_thread_t* thread = dtape_thread_for_xnu_thread(xthread);
+ libsimple_lock_lock(&thread->block_suspend_gate);
thread->xnu_thread.wait_result = wresult;
+ /* Match XNU's thread_unblock: a timer from a previous timed wait must
+ * not expire during the thread's next, untimed Mach receive. */
+ if (thread->xnu_thread.wait_timer_is_set) {
+ if (timer_call_cancel(&thread->xnu_thread.wait_timer)) {
+ thread->xnu_thread.wait_timer_active--;
+ }
+ thread->xnu_thread.wait_timer_is_set = FALSE;
+ }
+ thread->xnu_thread.state &= ~(TH_WAIT | TH_UNINT);
+ thread->xnu_thread.state |= TH_RUN;
dtape_hooks->thread_resume(thread->context);
+ libsimple_lock_unlock(&thread->block_suspend_gate);
return TRUE;
};
@@ -782,7 +800,7 @@ thread_set_state(
s64.dr6 = s->dr6;
s64.dr7 = s->dr7;
- return thread_set_state(thread, x86_DEBUG_STATE64, (thread_state_t) &s,
+ return thread_set_state(thread, x86_DEBUG_STATE64, (thread_state_t) &s64,
x86_DEBUG_STATE64_COUNT);
}
case x86_DEBUG_STATE64:
@@ -1293,7 +1311,7 @@ kern_return_t thread_info(thread_t xthread, thread_flavor_t flavor, thread_info_
};
default:
- dtape_stub_unsafe("Unimplemented flavor");
+ return KERN_INVALID_ARGUMENT;
}
};
@@ -1324,11 +1342,11 @@ kern_return_t thread_wire(host_priv_t host_priv, thread_t thread, boolean_t wire
};
kern_return_t thread_getstatus_to_user(thread_t thread, int flavor, thread_state_t tstate, mach_msg_type_number_t* count) {
- dtape_stub_unsafe();
+ return thread_get_state_internal(thread, flavor, tstate, count, TRUE);
};
kern_return_t thread_setstatus_from_user(thread_t thread, int flavor, thread_state_t tstate, mach_msg_type_number_t count) {
- dtape_stub_unsafe();
+ return thread_set_state_from_user(thread, flavor, tstate, count);
};
boolean_t thread_should_abort(thread_t thread) {
diff --git a/duct-tape/src/timer.c b/duct-tape/src/timer.c
index bbaab01..4f01234 100644
--- a/duct-tape/src/timer.c
+++ b/duct-tape/src/timer.c
@@ -53,8 +53,9 @@ uint64_t _rtc_nanotime_read(pal_rtc_nanotime_t* rntp) {
};
void dtape_timer_fired(void) {
- uint64_t next_deadline = timer_queue_expire(&timer_queue, mach_absolute_time());
- dtape_hooks->timer_arm(next_deadline, true);
+ /* Expiration rearms while holding the queue lock. Rearming here from
+ * its returned snapshot could overwrite a concurrent insertion. */
+ timer_queue_expire(&timer_queue, mach_absolute_time());
};
void timer_call_nosync_cpu(int cpu, void (*fn)(void* arg), void* arg) {
@@ -62,7 +63,8 @@ void timer_call_nosync_cpu(int cpu, void (*fn)(void* arg), void* arg) {
};
mpqueue_head_t* timer_queue_assign(uint64_t deadline) {
- dtape_hooks->timer_arm(deadline, false);
+ /* Arm only after insertion, under the queue lock. A short deadline
+ * can otherwise expire before its call has reached the queue. */
return &timer_queue;
};
diff --git a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c
index 7c2559b..202d5dc 100644
--- a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c
+++ b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c
@@ -77,6 +77,7 @@
#include <mach/message.h>
#include <mach/port.h>
#include <mach/vm_map.h>
+#include <darlingserver/duct-tape/log.h> /* darling-lol M58g1: dtape_log_error */
#include <mach/mach_vm.h>
#include <mach/vm_statistics.h>
@@ -2497,6 +2498,10 @@ ipc_kmsg_put(
}
if (copyoutmsg((const char *) kmsg->ikm_header, rcv_addr, size)) {
+ dtape_log_error("M58G ipc_kmsg_put copyout failed size=%u rcv_size=%u rcv_addr=0x%llx option=0x%x bits=0x%x id=%d task=%p map=%p",
+ size, (unsigned)rcv_size, (unsigned long long) rcv_addr, (unsigned) option,
+ (unsigned) kmsg->ikm_header->msgh_bits, (int) kmsg->ikm_header->msgh_id,
+ (void*) current_task(), (void*) current_map());
mr = MACH_RCV_INVALID_DATA;
size = 0;
} else {
@@ -3819,7 +3824,9 @@ ipc_kmsg_copyin_body(
goto clean_message;
}
- if ((size >= MSG_OOL_SIZE_SMALL) &&
+ /* Darling has a zero small-copy threshold. Empty OOL regions
+ * require no allocation and must not trip the overflow check. */
+ if (size != 0 && (size >= MSG_OOL_SIZE_SMALL) &&
(daddr->out_of_line.copy == MACH_MSG_PHYSICAL_COPY) &&
!(daddr->out_of_line.deallocate)) {
/*
diff --git a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c b/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c
index 98ad438..221a1be 100644
--- a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c
+++ b/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c
@@ -87,6 +87,7 @@
#include <ipc/port.h>
#include <ipc/ipc_mqueue.h>
#include <ipc/ipc_kmsg.h>
+#include <darlingserver/duct-tape/log.h> /* darling-lol M58bc: dtape_log_error */
#include <ipc/ipc_port.h>
#include <ipc/ipc_pset.h>
#include <ipc/ipc_space.h>
@@ -757,6 +758,25 @@ ipc_mqueue_post(
ipc_kmsg_trace_send(kmsg, option);
+ {
+ /* darling-lol M58bc: mach-message delivery trace. Logs the receiver's
+ * pid, message id and size for each enqueue, so we can see whether a
+ * message addressed to the renderer's port is delivered. Capped. */
+ ipc_port_t __m58bc_port = ip_from_mq(mqueue);
+ if (__m58bc_port != IP_NULL && ip_active(__m58bc_port)) {
+ int __m58bc_rpid = ipc_port_get_receiver_task(__m58bc_port, NULL);
+ mach_msg_size_t __m58bc_sz = (kmsg->ikm_header != 0) ? kmsg->ikm_header->msgh_size : 0;
+ int __m58bc_id = (kmsg->ikm_header != 0) ? (int) kmsg->ikm_header->msgh_id : 0;
+#if 0 /* darling-lol M58F173: disabled again (per-message dserver log overhead) */
+ static unsigned long __m58bc_n = 0;
+ if (__m58bc_n++ < 200000) {
+ dtape_log_error("M58BC post rpid=%d id=%d size=%u port=%p",
+ __m58bc_rpid, __m58bc_id, (unsigned) __m58bc_sz, (void*)__m58bc_port);
+ }
+#endif
+ }
+ }
+
/*
* While the msg queue is locked, we have control of the
* kmsg, so the ref in it for the port is still good.
@@ -786,6 +806,24 @@ ipc_mqueue_post(
WAITQ_KEEP_LOCKED);
/* waitq still locked, thread locked */
+ /* darling-lol M58RD: record whether the post found a blocked receiver
+ * or had to queue. A queued message while a thread is parked in
+ * ipc_mqueue_receive on the SAME port means a lost wakeup. */
+#if 0 /* darling-lol M58F173: disabled again (per-message dserver log overhead) */
+ {
+ static unsigned long __m58rd_n = 0;
+ if (__m58rd_n++ < 200000) {
+ ipc_port_t __m58rd_port = ip_from_mq(mqueue);
+ int __m58rd_rpid = (__m58rd_port != IP_NULL && ip_active(__m58rd_port))
+ ? ipc_port_get_receiver_task(__m58rd_port, NULL) : -1;
+ int __m58rd_id = (kmsg->ikm_header != 0) ? (int)kmsg->ikm_header->msgh_id : 0;
+ dtape_log_error("M58RD %s rpid=%d id=%d port=%p rcv=%p",
+ (receiver == THREAD_NULL) ? "QUEUED" : "HANDOFF",
+ __m58rd_rpid, __m58rd_id, (void*)__m58rd_port, (void*)receiver);
+ }
+ }
+#endif
+
if (receiver == THREAD_NULL) {
/*
* no receivers; queue kmsg if space still reserved
@@ -1222,6 +1260,23 @@ ipc_mqueue_receive_on_thread(
TURNSTILE_DELAYED_UPDATE);
}
+ /* darling-lol M58RE: receive-side trace. When a thread is about to block
+ * on a mach-message receive, log the receiver pid and the port it waits on
+ * (pointer identity). Pair with M58BC posts (same port=) to see whether the
+ * expected message is enqueued on this exact port. Capped. */
+#if 0 /* darling-lol M58F151: disabled (per-receive dserver log overhead) */
+ {
+ static unsigned long __m58re_n = 0;
+ if (__m58re_n++ < 200000) {
+ ipc_port_t __m58re_port = ip_from_mq(mqueue);
+ int __m58re_rpid = (__m58re_port != IP_NULL && ip_active(__m58re_port))
+ ? ipc_port_get_receiver_task(__m58re_port, NULL) : -1;
+ dtape_log_error("M58RE recv rpid=%d tid=%p port=%p opt=0x%x",
+ __m58re_rpid, (void*)thread, (void*)__m58re_port, (unsigned)option);
+ }
+ }
+#endif
+
thread_set_pending_block_hint(thread, kThreadWaitPortReceive);
wresult = waitq_assert_wait64_locked(&mqueue->imq_wait_queue,
IPC_MQUEUE_RECEIVE,
diff --git a/duct-tape/xnu/osfmk/ipc/ipc_object.c b/duct-tape/xnu/osfmk/ipc/ipc_object.c
index 5e2fdb8..2352d43 100644
--- a/duct-tape/xnu/osfmk/ipc/ipc_object.c
+++ b/duct-tape/xnu/osfmk/ipc/ipc_object.c
@@ -89,6 +89,7 @@
#include <ipc/ipc_hash.h>
#include <ipc/ipc_right.h>
#include <ipc/ipc_notify.h>
+#include <darlingserver/duct-tape/log.h> /* darling-lol M58DD: copyout diagnostics */
#include <ipc/ipc_port.h>
#include <ipc/ipc_pset.h>
@@ -1016,6 +1017,13 @@ ipc_object_copyout(
is_write_unlock(space);
out:
+ /* Successful copyouts are a hot path; diagnostic logging here slows
+ * normal Mach traffic and fills dserver.log during Riot startup. */
+ if (kr != KERN_SUCCESS) {
+ dtape_log_error("M58DD copyout-fail msgt=0x%x kr=%d",
+ (unsigned) msgt_name, (int) kr);
+ }
+
if (kr == KERN_SUCCESS) {
*namep = name;
} else if (IO_VALID(object)) {
diff --git a/duct-tape/xnu/osfmk/ipc/mach_msg.c b/duct-tape/xnu/osfmk/ipc/mach_msg.c
index f073413..0dd19db 100644
--- a/duct-tape/xnu/osfmk/ipc/mach_msg.c
+++ b/duct-tape/xnu/osfmk/ipc/mach_msg.c
@@ -76,7 +76,7 @@
#include <mach/message.h>
#include <mach/mig_errors.h>
#include <mach/mach_traps.h>
-
+#include <darlingserver/duct-tape/log.h> /* darling-lol M58g1: dtape_log_error */
#include <kern/kern_types.h>
#include <kern/assert.h>
#include <kern/cpu_number.h>
@@ -397,6 +397,14 @@ mach_msg_receive_results(
mr = ipc_kmsg_copyout(kmsg, space, map, MACH_MSG_BODY_NULL, option);
+ /* darling-lol M58g1: log receive-side failures to find the
+ * waitable-event receive failure (MACH_RCV_INVALID_DATA 5/5 in the
+ * harness repro, tools/m58g-machmsg-repro.c). */
+ if (mr != MACH_MSG_SUCCESS) {
+ dtape_log_error("M58G recv-err mr=0x%08x msgh_size=%u rcv_size=%u",
+ mr, kmsg->ikm_header->msgh_size, (unsigned)rcv_size);
+ }
+
trailer_size = ipc_kmsg_trailer_size(option, self);
if (mr != MACH_MSG_SUCCESS) {
@@ -779,6 +787,9 @@ msg_receive_error(
sizeof(trailer_template));
trailer_size = ipc_kmsg_trailer_size(option, self);
+ /* darling-lol M58g1: track msg_receive_error's copyout inputs. */
+ dtape_log_error("M58G receive-error trailer_size=%u rcv_size=%u",
+ (unsigned)trailer_size, (unsigned)rcv_size);
ipc_kmsg_add_trailer(kmsg, space, option, self,
seqno, TRUE, context);
diff --git a/duct-tape/xnu/osfmk/ipc/mach_port.c b/duct-tape/xnu/osfmk/ipc/mach_port.c
index deaa574..3adbde2 100644
--- a/duct-tape/xnu/osfmk/ipc/mach_port.c
+++ b/duct-tape/xnu/osfmk/ipc/mach_port.c
@@ -101,6 +101,7 @@
#if IMPORTANCE_INHERITANCE
#include <ipc/ipc_importance.h>
+#include <darlingserver/duct-tape/log.h> /* darling-lol M58DB: mod_refs diagnostics */
#endif
kern_return_t mach_port_get_attributes(ipc_space_t space, mach_port_name_t name,
@@ -999,12 +1000,18 @@ mach_port_mod_refs(
kr = ipc_right_lookup_write(space, name, &entry);
if (kr != KERN_SUCCESS) {
mach_port_guard_exception(name, 0, 0, kGUARD_EXC_INVALID_NAME);
+ dtape_log_error("M58DB mod_refs LOOKUP-FAIL name=%u right=%d delta=%d kr=%d",
+ (unsigned) name, (int) right, (int) delta, (int) kr);
return kr;
}
/* space is write-locked and active */
kr = ipc_right_delta(space, name, entry, right, delta); /* unlocks */
+ if (kr != KERN_SUCCESS) {
+ dtape_log_error("M58DB mod_refs DELTA-FAIL name=%u right=%d delta=%d kr=%d",
+ (unsigned) name, (int) right, (int) delta, (int) kr);
+ }
return kr;
}
diff --git a/duct-tape/xnu/osfmk/kern/ipc_kobject.c b/duct-tape/xnu/osfmk/kern/ipc_kobject.c
index 408cb36..bbddfca 100644
--- a/duct-tape/xnu/osfmk/kern/ipc_kobject.c
+++ b/duct-tape/xnu/osfmk/kern/ipc_kobject.c
@@ -148,6 +148,8 @@
#include <kern/sync_sema.h>
#include <kern/work_interval.h>
#include <kern/suid_cred.h>
+/* darling-lol M54: fileport kobject lives in duct-tape/src/fileport.c */
+extern void dtape_fileport_destroy(struct ipc_port* port);
#include <kern/task_ident.h>
#if HYPERVISOR
@@ -1045,6 +1047,11 @@ ipc_kobject_destroy(
suid_cred_destroy(port);
break;
+ /* darling-lol M54: fileports. */
+ case IKOT_FILEPORT:
+ dtape_fileport_destroy(port);
+ break;
+
default:
break;
}
diff --git a/duct-tape/xnu/osfmk/kern/ipc_misc.c b/duct-tape/xnu/osfmk/kern/ipc_misc.c
index 6e7638f..76f680e 100644
--- a/duct-tape/xnu/osfmk/kern/ipc_misc.c
+++ b/duct-tape/xnu/osfmk/kern/ipc_misc.c
@@ -127,7 +127,8 @@ fileport_notify(mach_msg_header_t *msg)
if (port->ip_srights == 0) {
ip_unlock(port);
- fileport_releasefg(fg);
+ /* Darling stores a host FD kobject, not a BSD fileglob.
+ * ipc_kobject_destroy performs its one close/free on destruction. */
ipc_port_dealloc_kernel(port);
} else {
ip_unlock(port);
diff --git a/duct-tape/xnu/osfmk/kern/timer_call.c b/duct-tape/xnu/osfmk/kern/timer_call.c
index 4983e26..fd37050 100644
--- a/duct-tape/xnu/osfmk/kern/timer_call.c
+++ b/duct-tape/xnu/osfmk/kern/timer_call.c
@@ -385,6 +385,13 @@ timer_call_entry_enqueue_deadline(
}
new_mpqueue->count++;
+#ifdef __DARLING__
+ /* Publish the actual queue head after insertion, while serialized with
+ * expiration and cancellation. The timerfd must never reflect a stale
+ * pre-insertion or post-expiration snapshot. */
+ timer_queue_cancel(new_mpqueue, deadline, thead->tc_pqlink.deadline);
+#endif
+
return old_mpqueue;
}
@@ -1003,6 +1010,9 @@ timer_queue_expire_with_options(
queue->earliest_soft_deadline = cur_deadline = UINT64_MAX;
}
+#ifdef __DARLING__
+ timer_queue_cancel(queue, deadline, cur_deadline);
+#endif
timer_queue_unlock(queue);
return cur_deadline;
diff --git a/duct-tape/xnu/osfmk/vm/vm_map.h b/duct-tape/xnu/osfmk/vm/vm_map.h
index 15fa8c8..4321a91 100644
--- a/duct-tape/xnu/osfmk/vm/vm_map.h
+++ b/duct-tape/xnu/osfmk/vm/vm_map.h
@@ -205,6 +205,12 @@ struct vm_named_entry {
#define NAMED_ENTRY_BT_DEPTH 16
void *named_entry_bt[NAMED_ENTRY_BT_DEPTH];
#endif /* VM_NAMED_ENTRY_LIST */
+ /* darling-lol: duct-tape shared backing. XNU's object model is not
+ * wired up here; instead an entry may carry a shared memfd descriptor
+ * (see dtape_map_shared_descriptor in duct-tape/src/memory.c) that is
+ * mapped into tasks on demand, giving true cross-task sharing.
+ * Opaque pointer to avoid header coupling; NULL when unused. */
+ void *dtape_descriptor;
};
/*
diff --git a/internal-include/darlingserver/logging.hpp b/internal-include/darlingserver/logging.hpp
index d46bdb4..9a58727 100644
--- a/internal-include/darlingserver/logging.hpp
+++ b/internal-include/darlingserver/logging.hpp
@@ -41,6 +41,7 @@ namespace DarlingServer {
Error = 4,
};
+ bool _enabled(Type type) const;
void _log(Type type, std::string message) const;
static std::string _typeToString(Type type);
@@ -57,6 +58,9 @@ namespace DarlingServer {
class Stream;
friend class Stream;
+ /// whether a message of this level (1 = debug .. 4 = error) would be written; cheap, for skipping formatting work
+ bool levelEnabled(int level) const { return _enabled(static_cast<Type>(level)); };
+
Stream debug() const;
Stream info() const;
Stream warning() const;
@@ -74,7 +78,7 @@ namespace DarlingServer {
private:
Type _type;
const Log& _log;
- std::ostringstream _buffer;
+ std::unique_ptr<std::ostringstream> _buffer;
Stream(Type type, const Log& log);
@@ -91,7 +95,9 @@ namespace DarlingServer {
template<class T>
std::enable_if_t<!std::is_base_of_v<Loggable, T>, Stream&> operator<<(const T& value) {
- _buffer << value;
+ if (_buffer) {
+ *_buffer << value;
+ }
return *this;
};
};
diff --git a/internal-include/darlingserver/message.hpp b/internal-include/darlingserver/message.hpp
index 708f3f3..fd46ab8 100644
--- a/internal-include/darlingserver/message.hpp
+++ b/internal-include/darlingserver/message.hpp
@@ -28,6 +28,7 @@
#include <mutex>
#include <optional>
#include <functional>
+#include <array>
namespace DarlingServer {
class Address {
@@ -71,6 +72,11 @@ namespace DarlingServer {
void _ensureDescriptorHeader(size_t descriptorSpace);
public:
+ /** Called once after the server's final fork/privilege drop, before workers.
+ * The server does not change its native identity afterwards. Generic Message
+ * users retain live credentials unless they explicitly initialize this cache.
+ */
+ static void initializeServerCredentials();
/**
* Default-initializes a Message.
*
@@ -161,13 +167,30 @@ namespace DarlingServer {
class MessageQueue {
private:
std::deque<Message> _messages;
+ // Retain unused receive storage; queued Messages own their buffers and FDs.
+ std::array<std::optional<Message>, 16> _receiveMessages;
mutable std::mutex _lock;
std::function<void()> _messageArrivalNotificationCallback = nullptr;
+ void _notifyMessageArrival();
+
public:
void setMessageArrivalNotificationCallback(std::function<void()> messageArrivalNotificationCallback);
void push(Message&& message);
+
+ /**
+ * Pushes a message and immediately tries to send as much of the queue as
+ * possible through the given socket from the calling thread.
+ *
+ * This avoids the event loop hop for messages that can be sent right away.
+ * Sending always starts at the head of the queue, so a direct send can
+ * never overtake messages queued by an earlier EAGAIN. When the socket
+ * would block, the message-arrival notification callback fires (once) so
+ * the receiving loop retries once the socket is writable again.
+ */
+ void pushAndSend(Message&& message, int socket);
+
std::optional<Message> pop();
bool sendMany(int socket);
diff --git a/internal-include/darlingserver/process.hpp b/internal-include/darlingserver/process.hpp
index c97be30..31772dc 100644
--- a/internal-include/darlingserver/process.hpp
+++ b/internal-include/darlingserver/process.hpp
@@ -94,6 +94,13 @@ namespace DarlingServer {
std::shared_ptr<FD> _vchrootDescriptor;
dtape_task_t* _dtapeTask;
std::weak_ptr<Process> _parentProcess;
+
+ // /proc/<pid>/maps, kept open for PROCMAP_QUERY region lookups (opening it per lookup cost more than the query).
+ // Guarded by its own mutex: a stale descriptor is closed and reopened, and a concurrent ioctl on a closed and
+ // reused descriptor number would hit an unrelated file.
+ mutable std::mutex _mapsMutex;
+ mutable int _mapsFd = -1;
+ int _procmapQuery(uint64_t address, uint64_t flags, void* query) const;
bool _startSuspended = false;
bool _pendingReplacement = false;
std::unordered_map<uintptr_t, std::shared_ptr<Kqchan>> _kqchannels;
diff --git a/internal-include/darlingserver/server.hpp b/internal-include/darlingserver/server.hpp
index b960f17..5f0e621 100644
--- a/internal-include/darlingserver/server.hpp
+++ b/internal-include/darlingserver/server.hpp
@@ -46,6 +46,9 @@ namespace DarlingServer {
WorkQueue<std::shared_ptr<Thread>> _workQueue;
bool _canRead = false;
bool _canWrite = true;
+ /* darling-lol: producers send their own replies (see sendMessage) unless
+ * DSERVER_DIRECT_REPLY=0 restored the old outbox wakeup hand-off. */
+ bool _directReply = true;
int _wakeupFD;
int _timerFD;
uint64_t _currentTimerDeadline = 0;
diff --git a/scripts/generate-rpc-wrappers.py b/scripts/generate-rpc-wrappers.py
index e35221c..1068426 100755
--- a/scripts/generate-rpc-wrappers.py
+++ b/scripts/generate-rpc-wrappers.py
@@ -268,6 +268,22 @@ calls = [
('socket', '@fd'),
]),
+ #
+ # darling-lol M54: fileports (Mojo FD-over-Mach transport)
+ #
+
+ ('fileport_makeport', [
+ ('fd', '@fd'),
+ ], [
+ ('port_name', 'uint32_t'),
+ ]),
+
+ ('fileport_makefd', [
+ ('port_name', 'uint32_t'),
+ ], [
+ ('fd', '@fd'),
+ ]),
+
#
# pthread cancelation
#
diff --git a/src/call.cpp b/src/call.cpp
index cc10f1d..7df0056 100644
--- a/src/call.cpp
+++ b/src/call.cpp
@@ -18,9 +18,20 @@
*/
#define _GNU_SOURCE 1
+#include <map>
+#include <vector>
+#include <algorithm>
+#include <chrono>
+#include <cstdio>
+#include <cstdlib>
#include <darlingserver/call.hpp>
#include <darlingserver/server.hpp>
#include <sys/uio.h>
+#include <cerrno>
+#include <unistd.h>
+#include <atomic>
+#include <cstdlib>
+#include <cstdio>
#include <darlingserver/logging.hpp>
#include <darlingserver/duct-tape.h>
@@ -29,16 +40,76 @@
#include <sys/syscall.h>
#include <darlingserver/kqchan.hpp>
+static bool m634TraceEnabled() {
+ static const bool enabled = [] {
+ const char* value = std::getenv("DARLING_M634_TRACE");
+ return value && value[0] == '1' && value[1] == '\0';
+ }();
+ return enabled;
+}
+
static DarlingServer::Log callLog("calls");
DarlingServer::Log DarlingServer::Call::rpcReplyLog("replies");
+// darling-lol: opt-in request statistics. With DSERVER_CALL_STATS=<file> set, every 5 s the server appends how many
+// requests of each call number arrived and which (pid, tid) pairs sent the most, then starts over. Costs one getenv
+// at startup and a branch per request when off. callFromMessage runs on the server's single message loop thread.
+static void callStatsRecord(unsigned number, int pid, int tid) {
+ static const char* path = getenv("DSERVER_CALL_STATS");
+ if (!path || !path[0]) {
+ return;
+ }
+ static std::map<unsigned, unsigned long> byNumber;
+ static std::map<std::pair<int, unsigned>, unsigned long> byThreadCall;
+ static std::map<std::pair<int, int>, unsigned long> byThread;
+ static unsigned long total = 0;
+ static auto last = std::chrono::steady_clock::now();
+
+ ++byNumber[number];
+ ++byThreadCall[{tid, number}];
+ ++byThread[{pid, tid}];
+ ++total;
+
+ auto now = std::chrono::steady_clock::now();
+ if (now - last < std::chrono::seconds(5)) {
+ return;
+ }
+ double seconds = std::chrono::duration<double>(now - last).count();
+ last = now;
+
+ if (FILE* file = fopen(path, "a")) {
+ fprintf(file, "calls: %lu in %.1fs (%.0f/s)\n by call number:", total, seconds, total / seconds);
+ std::vector<std::pair<unsigned long, unsigned>> numbers;
+ for (auto& [num, count]: byNumber) numbers.push_back({count, num});
+ std::sort(numbers.rbegin(), numbers.rend());
+ for (size_t i = 0; i < numbers.size() && i < 12; ++i) fprintf(file, " #%u=%lu", numbers[i].second, numbers[i].first);
+ fprintf(file, "\n by pid/tid:");
+ std::vector<std::pair<unsigned long, std::pair<int, int>>> threads;
+ for (auto& [id, count]: byThread) threads.push_back({count, id});
+ std::sort(threads.rbegin(), threads.rend());
+ for (size_t i = 0; i < threads.size() && i < 12; ++i) fprintf(file, " %d/%d=%lu", threads[i].second.first, threads[i].second.second, threads[i].first);
+ fprintf(file, "\n by tid/call:");
+ std::vector<std::pair<unsigned long, std::pair<int, unsigned>>> pairs;
+ for (auto& [id, count]: byThreadCall) pairs.push_back({count, id});
+ std::sort(pairs.rbegin(), pairs.rend());
+ for (size_t i = 0; i < pairs.size() && i < 24; ++i) fprintf(file, " %d:#%u=%lu", pairs[i].second.first, pairs[i].second.second, pairs[i].first);
+ fprintf(file, "\n");
+ fclose(file);
+ }
+ byThreadCall.clear();
+ byNumber.clear();
+ byThread.clear();
+ total = 0;
+};
+
std::shared_ptr<DarlingServer::Call> DarlingServer::Call::callFromMessage(Message&& requestMessage) {
if (requestMessage.data().size() < sizeof(dserver_rpc_callhdr_t)) {
throw std::invalid_argument("Message buffer was too small for call header");
}
dserver_rpc_callhdr_t* header = reinterpret_cast<dserver_rpc_callhdr_t*>(requestMessage.data().data());
+ callStatsRecord(header->number, header->pid, header->tid);
std::shared_ptr<Call> result = nullptr;
std::shared_ptr<Process> process = nullptr;
std::shared_ptr<Thread> thread = nullptr;
@@ -123,9 +194,43 @@ std::shared_ptr<DarlingServer::Call> DarlingServer::Call::callFromMessage(Messag
}
}
- auto pidString = (process) ? (std::to_string(process->id()) + " (" + std::to_string(process->nsid()) + ")") : (std::to_string(header->pid) + " (-1)");
- auto tidString = (thread) ? (std::to_string(thread->id()) + " (" + std::to_string(thread->nsid()) + ")") : (std::to_string(header->tid) + " (-1)");
- callLog.debug() << "Received call #" << header->number << " (" << dserver_callnum_to_string(header->number) << ") from PID " << pidString << ", TID " << tidString << callLog.endLog;
+ if (callLog.levelEnabled(1)) {
+ auto pidString = (process) ? (std::to_string(process->id()) + " (" + std::to_string(process->nsid()) + ")") : (std::to_string(header->pid) + " (-1)");
+ auto tidString = (thread) ? (std::to_string(thread->id()) + " (" + std::to_string(thread->nsid()) + ")") : (std::to_string(header->tid) + " (-1)");
+ callLog.debug() << "Received call #" << header->number << " (" << dserver_callnum_to_string(header->number) << ") from PID " << pidString << ", TID " << tidString << callLog.endLog;
+ }
+
+ /* darling-lol M634: rate-limited trace of EVERY call through dserver, to
+ * see which RPCs the launcher's blocked service threads are waiting on
+ * and whether an S2C/interrupt round-trip is wedged. */
+ if (m634TraceEnabled()) {
+ static std::atomic<unsigned> m634AllCount{0};
+ unsigned seen = m634AllCount.fetch_add(1);
+ if (seen < 120 || seen % 64 == 0) {
+ fprintf(stderr, "M634RPC line=%u nr=%d pid=%d ns=%d tid=%d nstid=%d\n",
+ seen, (int)header->number, requestMessage.pid(),
+ process ? (int)process->nsid() : -1,
+ header->tid, thread ? (int)thread->nsid() : -1);
+ }
+ }
+
+ /* darling-lol M634: bounded trace of the storm calls (semaphore_timedwait
+ * and pthread_canceled) to diagnose the League-launch wedge. */
+ if (m634TraceEnabled() && (header->number == dserver_callnum_semaphore_timedwait || header->number == dserver_callnum_pthread_canceled)) {
+ static std::atomic<unsigned> m634CallCount{0};
+ unsigned seen = m634CallCount.fetch_add(1);
+ if (seen < 200 || seen % 256 == 0) {
+ if (header->number == dserver_callnum_semaphore_timedwait) {
+ auto call64 = reinterpret_cast<const dserver_rpc_call_semaphore_timedwait_t*>(requestMessage.data().data());
+ fprintf(stderr, "M634CALL line=%u nr=64 pid=%d tid=%d name=%u sec=%u nsec=%u\n",
+ seen, requestMessage.pid(), header->tid,
+ call64->body.wait_name, call64->body.sec, call64->body.nsec);
+ } else {
+ fprintf(stderr, "M634CALL line=%u nr=33 pid=%d tid=%d\n",
+ seen, requestMessage.pid(), header->tid);
+ }
+ }
+ }
if (header->number == dserver_callnum_s2c) {
// this is an S2C reply
@@ -694,6 +799,60 @@ void DarlingServer::Call::KqchanProcOpen::processCall() {
_sendReply(code, socket);
};
+void DarlingServer::Call::FileportMakeport::processCall() {
+ int code = 0;
+ uint32_t port_name = 0;
+
+ if (auto thread = _thread.lock()) {
+ if (auto process = thread->process()) {
+ // _body.fd arrives via SCM_RIGHTS; the framework closes it
+ // after this call, so dup for the kobject's own lifecycle.
+ int fd = dup(_body.fd);
+ if (fd < 0) {
+ code = -errno;
+ } else {
+ int kr = dtape_fileport_makeport(process->_dtapeTask, fd, &port_name);
+ if (kr != 0) {
+ code = -EINVAL;
+ ::close(fd);
+ }
+ }
+ } else {
+ code = -ESRCH;
+ }
+ } else {
+ code = -ESRCH;
+ }
+
+ _sendReply(code, port_name);
+};
+
+void DarlingServer::Call::FileportMakefd::processCall() {
+ int code = 0;
+ int fd = -1;
+
+ if (auto thread = _thread.lock()) {
+ if (auto process = thread->process()) {
+ int held = -1;
+ int kr = dtape_fileport_makefd(process->_dtapeTask, _body.port_name, &held);
+ if (kr != 0) {
+ code = -EINVAL;
+ } else {
+ // dtape_fileport_makefd already returns an owned duplicate.
+ // Transfer it to Message; its destructor closes the server
+ // copy after SCM_RIGHTS gives the receiver its own descriptor.
+ fd = held;
+ }
+ } else {
+ code = -ESRCH;
+ }
+ } else {
+ code = -ESRCH;
+ }
+
+ _sendReply(code, fd);
+};
+
void DarlingServer::Call::ForkWaitForChild::processCall() {
int code = 0;
@@ -714,6 +873,24 @@ void DarlingServer::Call::Sigprocess::processCall() {
int code = 0;
int newBSDSignal = 0;
+ // darling-lol M34 TMPLOG: every guest signal delivered through sigexc.
+ // For the RC UX SIGSEGV this is the reliable place to see the crash:
+ // signal numbers + si_code + si_addr. RIP is not read here yet, but
+ // si_addr + code identify the fault class. (Chromium/Electron
+ // sigexc-routes its fatal signals through this RPC.)
+ std::string lolExe = "?";
+ if (auto th = _thread.lock()) {
+ if (auto pr = th->process())
+ lolExe = pr->executablePath();
+ }
+ callLog.error() << "LOLSIG processSignal bsd=" << _body.bsd_signal_number
+ << " linux=" << _body.linux_signal_number
+ << " code=" << _body.code
+ << " addr=0x" << std::hex << _body.signal_address << std::dec
+ << " ts=0x" << std::hex << _body.thread_state << std::dec
+ << " exe=" << lolExe
+ << callLog.endLog;
+
if (auto thread = _thread.lock()) {
try {
thread->processSignal(_body.bsd_signal_number, _body.linux_signal_number, _body.code, _body.signal_address, _body.thread_state, _body.float_state);
diff --git a/src/darlingserver.cpp b/src/darlingserver.cpp
index f79f057..54c42aa 100644
--- a/src/darlingserver.cpp
+++ b/src/darlingserver.cpp
@@ -497,6 +497,45 @@ static void handle_sigusr1(int signum) {
};
#endif
+/* darling-lol M58f2b: darlingserver was observed exiting silently ~100s into
+ * direct-UX runs; the monitor caught exit_code=15 — a SIGTERM sent by an
+ * unknown host process. Log the sender (siginfo.si_pid + its comm/cmdline)
+ * instead of dying, so the culprit is identified. Not strictly
+ * async-signal-safe, but this is a diagnostic for a fatal path. */
+static void handle_sigterm_diagnostic(int signum, siginfo_t* info, void* uctx) {
+ int fd = open("/tmp/dserver-signal.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
+ if (fd >= 0) {
+ dprintf(fd, "darlingserver pid=%d got %s from sender pid=%ld uid=%ld\n",
+ getpid(),
+ signum == SIGTERM ? "SIGTERM" : (signum == SIGINT ? "SIGINT" : "signal"),
+ info ? (long) info->si_pid : -1L,
+ info ? (long) info->si_uid : -1L);
+ if (info && info->si_pid > 0) {
+ char path[64], comm[256], cmdline[1024];
+ snprintf(path, sizeof(path), "/proc/%ld/comm", (long) info->si_pid);
+ int cfd = open(path, O_RDONLY);
+ if (cfd >= 0) {
+ ssize_t n = read(cfd, comm, sizeof(comm) - 1);
+ if (n > 0) { comm[n - 1] = '\0'; dprintf(fd, " sender comm: %s\n", comm); }
+ close(cfd);
+ }
+ snprintf(path, sizeof(path), "/proc/%ld/cmdline", (long) info->si_pid);
+ cfd = open(path, O_RDONLY);
+ if (cfd >= 0) {
+ ssize_t n = read(cfd, cmdline, sizeof(cmdline) - 1);
+ if (n > 0) {
+ cmdline[n] = '\0';
+ for (ssize_t i = 0; i < n; ++i) { if (cmdline[i] == '\0') cmdline[i] = ' '; }
+ dprintf(fd, " sender cmdline: %s\n", cmdline);
+ }
+ close(cfd);
+ }
+ }
+ close(fd);
+ }
+ /* keep running: the goal is to survive teardown races and name the sender */
+};
+
int main(int argc, char** argv) {
const char* prefix = NULL;
uid_t originalUID = -1;
@@ -515,6 +554,30 @@ int main(int argc, char** argv) {
char putOld[4096];
char *p;
+ /* darling-lol M58f2: never die from SIGPIPE. The server writes to
+ * client sockets (MessageQueue::sendMany's sendmmsg, RPC replies,
+ * async-writer) whose peers are guest processes that can be torn
+ * down at any moment (Chromium helper churn, crashes). The default
+ * SIGPIPE disposition kills the whole server SILENTLY — no core,
+ * no log — which then wedges the entire container (every guest RPC
+ * fails with ECONNREFUSED). The write paths already handle EPIPE /
+ * ECONNREFUSED errors gracefully (e.g. message.cpp sendMany drops
+ * the dead peer's message); they just never got a chance to run.
+ * Ignoring SIGPIPE makes those handlers reachable. */
+ signal(SIGPIPE, SIG_IGN);
+
+ /* darling-lol M58f2b: catch SIGTERM/SIGINT and log the sender instead
+ * of dying (see handle_sigterm_diagnostic above). */
+ {
+ struct sigaction sa_diag;
+ memset(&sa_diag, 0, sizeof(sa_diag));
+ sa_diag.sa_sigaction = handle_sigterm_diagnostic;
+ sa_diag.sa_flags = SA_SIGINFO | SA_RESTART;
+ sigemptyset(&sa_diag.sa_mask);
+ sigaction(SIGTERM, &sa_diag, NULL);
+ sigaction(SIGINT, &sa_diag, NULL);
+ }
+
if (argc < 6) {
fprintf(stderr, "darlingserver is not meant to be started manually\n");
exit(1);
@@ -577,8 +640,15 @@ int main(int argc, char** argv) {
// now set our increased rlimit
if (setrlimit(RLIMIT_NOFILE, &increased_limit) != 0) {
- fprintf(stderr, "Warning: failed to increase FD rlimit: %s\n", strerror(errno));
- //exit(1);
+ // darling-lol: a rootless user namespace cannot raise the hard limit.
+ // Still raise the soft limit to it: every named memory entry holds a
+ // memfd here, and Chromium's UX restart pushes past a 1024 soft limit,
+ // failing memfd_create with EMFILE.
+ increased_limit.rlim_cur = increased_limit.rlim_max = default_limit.rlim_max;
+ if (setrlimit(RLIMIT_NOFILE, &increased_limit) != 0) {
+ fprintf(stderr, "Warning: failed to increase FD rlimit: %s\n", strerror(errno));
+ //exit(1);
+ }
}
#endif
@@ -745,6 +815,13 @@ int main(int argc, char** argv) {
#endif
// create the server
+ // All native forks and privilege changes are complete. Snapshot only our
+ // fixed transport identity; received/per-message credentials stay independent.
+ DarlingServer::Message::initializeServerCredentials();
+ // Explicit desktop-debugging opt-in, after dropping elevated privileges.
+ if (getenv("RCLOL_DEBUG_ATTACH")) {
+ prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);
+ }
auto server = new DarlingServer::Server(prefix);
// tell the child to go ahead; the socket has been created
diff --git a/src/kqchan.cpp b/src/kqchan.cpp
index 7f766a2..1597a8c 100644
--- a/src/kqchan.cpp
+++ b/src/kqchan.cpp
@@ -13,6 +13,27 @@ static DarlingServer::Log kqchanMachPortLog("kqchan:mach_port");
static DarlingServer::Log kqchanProcLog("kqchan:proc");
static std::atomic_uint64_t kqchanDebugIDCounter = 0;
+// Opt-in bounded metadata trace; never records Mach message contents.
+#include <unistd.h>
+#include <cstdlib>
+#include <cstdio>
+#include <cstring>
+static void riotKqTrace(char stage, uint64_t channel, int socket, long value = 0) {
+ static int fd = [] {
+ if (!std::getenv("RCLOL_KQTRACE")) return -1;
+ char path[96]; std::snprintf(path, sizeof(path), "/tmp/riot-kqtrace-%d.log", getpid());
+ return open(path, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, 0600);
+ }();
+ if (fd < 0) return;
+ static std::atomic_uint64_t next{0};
+ uint64_t index = next.fetch_add(1);
+ char row[128]; std::memset(row, ' ', sizeof(row));
+ int n = std::snprintf(row, sizeof(row), "%llu %c channel=%llu fd=%d value=%ld", (unsigned long long)index, stage, (unsigned long long)channel, socket, value);
+ if (n >= 0 && n < 127) row[n] = ' ';
+ row[127] = '\n';
+ pwrite(fd, row, sizeof(row), (index % 8192) * sizeof(row));
+}
+
//
// base class
//
@@ -67,6 +88,7 @@ int DarlingServer::Kqchan::setup() {
throw std::system_error(ret, std::generic_category());
}
+ riotKqTrace('S', _debugID, _socket->fd());
std::weak_ptr<Kqchan> weakThis = sharedFromRoot();
_outbox.setMessageArrivalNotificationCallback([weakThis]() {
@@ -84,6 +106,7 @@ int DarlingServer::Kqchan::setup() {
// we can send all the messages in the same context that they were pushed
do {
self->_canSend = self->_outbox.sendMany(self->_socket->fd());
+ riotKqTrace('O', self->_debugID, self->_socket->fd(), self->_canSend);
} while (self->_canSend && !self->_outbox.empty());
});
@@ -119,6 +142,7 @@ int DarlingServer::Kqchan::setup() {
kqchanLog.debug() << *self << ": socket has pending incoming messages" << kqchanLog.endLog;
+ riotKqTrace('I', self->_debugID, self->_socket->fd());
// receive them all
while (self->_inbox.receiveMany(self->_socket->fd()));
@@ -135,6 +159,7 @@ int DarlingServer::Kqchan::setup() {
kqchanLog.debug() << *self << ": socket is now writable; sending all pending outgoing messages" << kqchanLog.endLog;
do {
self->_canSend = self->_outbox.sendMany(self->_socket->fd());
+ riotKqTrace('O', self->_debugID, self->_socket->fd(), self->_canSend);
} while (self->_canSend && !self->_outbox.empty());
}
});
@@ -343,6 +368,7 @@ void DarlingServer::Kqchan::MachPort::_modify(uint64_t receiveBuffer, uint64_t r
};
void DarlingServer::Kqchan::MachPort::_read(uint64_t defaultBuffer, uint64_t defaultBufferSize, pid_t nstid) {
+ riotKqTrace('R', _debugID, _socket->fd(), nstid);
kqchanMachPortLog.debug() << *this << ": received read request with {defaultBuffer=" << defaultBuffer << ",defaultBufferSize=" << defaultBufferSize << "}" << kqchanMachPortLog.endLog;
{
@@ -380,6 +406,7 @@ void DarlingServer::Kqchan::MachPort::_read(uint64_t defaultBuffer, uint64_t def
auto self = shared_from_this();
Thread::kernelAsync([self, thread, defaultBuffer, defaultBufferSize]() {
+ riotKqTrace('K', self->_debugID, self->_socket->fd());
Message msg(sizeof(dserver_kqchan_reply_mach_port_read_t), 0, self->_checkForEventsAsyncFactory());
kqchanMachPortLog.debug() << *self << ": handling read request in microthread" << kqchanMachPortLog.endLog;
@@ -395,8 +422,11 @@ void DarlingServer::Kqchan::MachPort::_read(uint64_t defaultBuffer, uint64_t def
reply->header.code = 0xdead;
}
Thread::currentThread()->impersonate(nullptr);
+ riotKqTrace('F', self->_debugID, self->_socket->fd(), reply->header.code);
+ riotKqTrace('P', self->_debugID, self->_socket->fd());
self->_outbox.push(std::move(msg));
+ riotKqTrace('D', self->_debugID, self->_socket->fd());
// now let's send any deferred notifications we might have
self->_sendDeferredNotification();
diff --git a/src/logging.cpp b/src/logging.cpp
index 7b8000f..dbaf832 100644
--- a/src/logging.cpp
+++ b/src/logging.cpp
@@ -22,6 +22,8 @@
#include <darlingserver/thread.hpp>
#include <darlingserver/process.hpp>
#include <filesystem>
+#include <cstring>
+#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
@@ -45,7 +47,8 @@ DarlingServer::Log::Log(std::string category):
DarlingServer::Log::Stream::Stream(Type type, const Log& log):
_type(type),
- _log(log)
+ _log(log),
+ _buffer(log._enabled(type) ? std::make_unique<std::ostringstream>() : nullptr)
{};
DarlingServer::Log::Stream::~Stream() {
@@ -53,17 +56,22 @@ DarlingServer::Log::Stream::~Stream() {
};
DarlingServer::Log::Stream& DarlingServer::Log::Stream::operator<<(EndLog value) {
- auto str = _buffer.str();
+ if (!_buffer) {
+ return *this;
+ }
+ auto str = _buffer->str();
if (!str.empty()) {
_log._log(_type, str);
- _buffer.str(std::string());
- _buffer.clear();
+ _buffer->str(std::string());
+ _buffer->clear();
}
return *this;
};
DarlingServer::Log::Stream& DarlingServer::Log::Stream::operator<<(const Loggable& loggable) {
- loggable.logToStream(*this);
+ if (_buffer) {
+ loggable.logToStream(*this);
+ }
return *this;
};
@@ -98,20 +106,7 @@ std::string DarlingServer::Log::_typeToString(Type type) {
}
};
-void DarlingServer::Log::_log(Type type, std::string message) const {
- // NOTE: we use POSIX file APIs because we want to append each message to the log file atomically,
- // and as far as i can tell, C++ fstreams provide no such guarantee (that they won't write in chunks).
- static int logFile = []() {
- std::filesystem::path path(Server::sharedInstance().prefix() + "/private/var/log/dserver.log");
- std::filesystem::create_directories(path.parent_path());
- return open(path.c_str(), O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
- }();
-
- static bool logToStderr = []() {
- auto val = getenv("DSERVER_LOG_STDERR");
- return val && strlen(val) >= 1 && (val[0] == 't' || val[0] == 'T' || val[0] == '1');
- }();
-
+bool DarlingServer::Log::_enabled(Type type) const {
static Type logMinLevel = []() {
auto val = getenv("DSERVER_LOG_LEVEL");
Type level = DEFAULT_LOG_CUTOFF;
@@ -129,7 +124,24 @@ void DarlingServer::Log::_log(Type type, std::string message) const {
return level;
}();
- if ((type < logMinLevel && !_alwaysLog) || logFile < 0) {
+ return _alwaysLog || type >= logMinLevel;
+};
+
+void DarlingServer::Log::_log(Type type, std::string message) const {
+ // NOTE: we use POSIX file APIs because we want to append each message to the log file atomically,
+ // and as far as i can tell, C++ fstreams provide no such guarantee (that they won't write in chunks).
+ static int logFile = []() {
+ std::filesystem::path path(Server::sharedInstance().prefix() + "/private/var/log/dserver.log");
+ std::filesystem::create_directories(path.parent_path());
+ return open(path.c_str(), O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+ }();
+
+ static bool logToStderr = []() {
+ auto val = getenv("DSERVER_LOG_STDERR");
+ return val && strlen(val) >= 1 && (val[0] == 't' || val[0] == 'T' || val[0] == '1');
+ }();
+
+ if (!_enabled(type) || logFile < 0) {
return;
}
diff --git a/src/message.cpp b/src/message.cpp
index 414c1d6..a7f5a94 100644
--- a/src/message.cpp
+++ b/src/message.cpp
@@ -24,6 +24,25 @@
#include <new>
#include <mutex>
#include <system_error>
+#include <pthread.h>
+#include <cstdio>
+#include <cstring>
+
+namespace {
+ std::optional<struct ucred> serverCredentials;
+ struct ucred outgoingCredentials() {
+ if (serverCredentials) return *serverCredentials;
+ return {getpid(), getuid(), getgid()};
+ }
+}
+
+void DarlingServer::Message::initializeServerCredentials() {
+ if (serverCredentials) throw std::logic_error("Server credentials already initialized");
+ // A future fork must never send the parent's cached PID from its child.
+ int result = pthread_atfork(nullptr, nullptr, [] { serverCredentials.reset(); });
+ if (result) throw std::system_error(result, std::generic_category(), "pthread_atfork");
+ serverCredentials = outgoingCredentials();
+}
DarlingServer::Address::Address() {
_address.sun_family = AF_UNIX;
@@ -55,6 +74,8 @@ void DarlingServer::Address::setRawSize(size_t newRawSize) {
DarlingServer::Message::Message(size_t bufferSpace, size_t descriptorSpace, std::function<void()> sendNotificationCallback):
_sendNotificationCallback(sendNotificationCallback)
{
+ // Allocate the vector first: if it throws, there is no raw control buffer to leak.
+ _buffer.resize(bufferSpace);
size_t controlLen = CMSG_SPACE(sizeof(struct ucred)) + (descriptorSpace > 0 ? CMSG_SPACE(sizeof(int) * descriptorSpace) : 0);
_controlHeader = static_cast<decltype(_controlHeader)>(malloc(controlLen));
@@ -65,10 +86,7 @@ DarlingServer::Message::Message(size_t bufferSpace, size_t descriptorSpace, std:
_controlHeader->cmsg_len = CMSG_LEN(sizeof(struct ucred));
_controlHeader->cmsg_level = SOL_SOCKET;
_controlHeader->cmsg_type = SCM_CREDENTIALS;
- struct ucred ourCreds;
- ourCreds.pid = getpid();
- ourCreds.uid = getuid();
- ourCreds.gid = getgid();
+ struct ucred ourCreds = outgoingCredentials();
// the cmsg man page says memcpy should be used with CMSG_DATA instead of direct pointer access
memcpy(CMSG_DATA(_controlHeader), &ourCreds, sizeof(ourCreds));
@@ -83,8 +101,6 @@ DarlingServer::Message::Message(size_t bufferSpace, size_t descriptorSpace, std:
}
}
- _buffer.resize(bufferSpace);
-
_header.msg_name = &_socketAddress.raw();
_header.msg_namelen = _socketAddress.rawSize();
_header.msg_iov = &_dataDescriptor;
@@ -348,10 +364,7 @@ void DarlingServer::Message::_ensureCredentialsHeader() {
credHeader->cmsg_level = SOL_SOCKET;
credHeader->cmsg_type = SCM_CREDENTIALS;
- struct ucred creds;
- creds.pid = getpid();
- creds.uid = getuid();
- creds.gid = getgid();
+ struct ucred creds = outgoingCredentials();
memcpy(CMSG_DATA(credHeader), &creds, sizeof(struct ucred));
};
@@ -496,13 +509,52 @@ std::optional<DarlingServer::Message> DarlingServer::MessageQueue::pop() {
}
};
+// darling-lol M58ak: log small (control) messages to/from clients so the
+// dserver's request/reply routing can be correlated. Writes to a host-visible
+// path from inside the guest. Size-filtered (<= 64) to keep volume low.
+static void m58ak_log_message(const char* dir, int socket, const struct sockaddr_un* addr, const void* data, size_t size) {
+ /* darling-lol M58aw: log only pthread_canceled (call 33) messages, both
+ * directions, with the destination address, to see whether the dserver
+ * emits the replies the client is missing. */
+ if (size < sizeof(int) || data == 0) {
+ return;
+ }
+ if (*(const int*) data != 33) {
+ return;
+ }
+ /* darling-lol: replies from workers and receives on the main loop can now
+ * reach this logging helper concurrently; serialize the lazy open and the
+ * shared counter. */
+ static std::mutex m58akLogLock;
+ std::scoped_lock m58akLock(m58akLogLock);
+ static FILE* f = nullptr;
+ static unsigned long n = 0;
+ if (!f) {
+ f = fopen("/tmp/opencode/m58ak.log", "a");
+ if (!f) {
+ f = fopen("/Volumes/SystemRoot/tmp/opencode/m58ak.log", "a");
+ }
+ }
+ if (!f || n++ > 300000) {
+ return;
+ }
+ char hex[20];
+ hex[0] = '\0';
+ if (addr) {
+ for (int i = 0; i < 8; ++i) {
+ snprintf(hex + i * 2, 3, "%02x", (unsigned char) addr->sun_path[i]);
+ }
+ }
+ fprintf(f, "M58AK %s sock=%d addr=%s size=%zu\n", dir, socket, hex, size);
+ fflush(f);
+}
+
bool DarlingServer::MessageQueue::sendMany(int socket) {
bool canSendMore = true;
std::scoped_lock lock(_lock);
struct mmsghdr mmsgs[16];
size_t len = 0;
int ret = 0;
-
while (ret >= 0) {
len = 0;
for (size_t i = 0; i < _messages.size() && i < sizeof(mmsgs) / sizeof(*mmsgs); ++i) {
@@ -538,7 +590,7 @@ bool DarlingServer::MessageQueue::sendMany(int socket) {
break;
} else if (errno == EINTR) {
ret = 0;
- } else if (errno == EPIPE || errno == ECONNREFUSED) {
+ } else if (errno == EPIPE || errno == ECONNREFUSED || errno == ENOENT) {
// this means that peer of the first message we tried to send has died.
// we'll just drop that message and try the others.
_messages.pop_front();
@@ -549,6 +601,7 @@ bool DarlingServer::MessageQueue::sendMany(int socket) {
}
for (size_t i = 0; i < ret; ++i) {
+ m58ak_log_message("send", socket, &_messages.front().address().raw(), _messages.front().data().data(), _messages.front().data().size());
auto cb = _messages.front().sendNotificationCallback();
_messages.pop_front();
if (cb) {
@@ -567,10 +620,11 @@ bool DarlingServer::MessageQueue::receiveMany(int socket) {
int ret = 0;
while (ret >= 0) {
- std::array<Message, sizeof(mmsgs) / sizeof(*mmsgs)> messages;
-
- for (size_t i = 0; i < messages.size(); ++i) {
- mmsgs[i].msg_hdr = messages[i].rawHeader();
+ for (size_t i = 0; i < _receiveMessages.size(); ++i) {
+ if (!_receiveMessages[i]) {
+ _receiveMessages[i].emplace();
+ }
+ mmsgs[i].msg_hdr = _receiveMessages[i]->rawHeader();
mmsgs[i].msg_len = 0;
}
@@ -587,11 +641,25 @@ bool DarlingServer::MessageQueue::receiveMany(int socket) {
}
}
- for (size_t i = 0; i < ret; ++i) {
- messages[i].rawHeader() = mmsgs[i].msg_hdr;
- messages[i].data().resize(mmsgs[i].msg_len);
- messages[i].setAddress(Address(*(const struct sockaddr_un*)mmsgs[i].msg_hdr.msg_name, mmsgs[i].msg_hdr.msg_namelen));
- _messages.push_back(std::move(messages[i]));
+ // Adopt every returned control header before anything can throw, so all
+ // received descriptors have an owner even if deque allocation later fails.
+ for (size_t i = 0; i < static_cast<size_t>(ret); ++i) {
+ _receiveMessages[i]->rawHeader() = mmsgs[i].msg_hdr;
+ }
+ struct ReceivedMessages {
+ decltype(_receiveMessages)& slots;
+ size_t count;
+ ~ReceivedMessages() {
+ for (size_t i = 0; i < count; ++i) slots[i].reset();
+ }
+ } receivedMessages{_receiveMessages, static_cast<size_t>(ret)};
+
+ for (size_t i = 0; i < static_cast<size_t>(ret); ++i) {
+ auto& message = *_receiveMessages[i];
+ message.data().resize(mmsgs[i].msg_len);
+ message.setAddress(Address(*(const struct sockaddr_un*)mmsgs[i].msg_hdr.msg_name, mmsgs[i].msg_hdr.msg_namelen));
+ m58ak_log_message("recv", socket, &message.address().raw(), message.data().data(), message.data().size());
+ _messages.push_back(std::move(message));
}
}
@@ -615,3 +683,43 @@ bool DarlingServer::MessageQueue::empty() const {
std::unique_lock lock(_lock);
return _messages.empty();
};
+
+void DarlingServer::MessageQueue::_notifyMessageArrival() {
+ std::function<void()> callback;
+
+ {
+ std::scoped_lock lock(_lock);
+ if (_messages.empty()) {
+ return;
+ }
+ callback = _messageArrivalNotificationCallback;
+ }
+
+ if (callback) {
+ callback();
+ }
+};
+
+void DarlingServer::MessageQueue::pushAndSend(Message&& message, int socket) {
+ {
+ std::scoped_lock lock(_lock);
+ _messages.push_back(std::move(message));
+ }
+
+ bool canSendMore = true;
+
+ try {
+ // send from this thread, but only by draining the queue in order, so that
+ // a direct send can never overtake messages queued by an earlier EAGAIN
+ canSendMore = sendMany(socket);
+ } catch (...) {
+ // the messages remain queued; the receiving loop retries and reports the error
+ _notifyMessageArrival();
+ throw;
+ }
+
+ if (!canSendMore) {
+ // the socket is full; the receiving loop has to retry once it is writable
+ _notifyMessageArrival();
+ }
+};
diff --git a/src/process.cpp b/src/process.cpp
index 9cd7b81..180f372 100644
--- a/src/process.cpp
+++ b/src/process.cpp
@@ -25,9 +25,15 @@
#include <darlingserver/logging.hpp>
#include <fstream>
+#include <atomic>
#include <regex>
+#include <cstdio>
+#include <cstring>
#include <sys/mman.h>
+#include <signal.h> /* darling-lol M58DS: kill(0) liveness probe */
+#include <stdio.h> /* darling-lol M58DS: /proc state read */
+#include <string.h>
static DarlingServer::Log processLog("process");
@@ -109,6 +115,10 @@ DarlingServer::Process::Process(KernelProcessConstructorTag tag):
DarlingServer::Process::~Process() {
processLog.info() << *this << ": process being destroyed" << processLog.endLog;
+ if (_mapsFd >= 0) {
+ close(_mapsFd);
+ _mapsFd = -1;
+ }
};
DarlingServer::Process::ID DarlingServer::Process::id() const {
@@ -245,6 +255,44 @@ bool DarlingServer::Process::_readOrWriteMemory(bool isWrite, uintptr_t remoteAd
<< strerror(code)
<< ")"
<< processMemoryAccessLog.endLog;
+ /* darling-lol M58DS: log why the kernel disagrees with isDead(). */
+ {
+ char m58path[64];
+ char m58comm[64] = "?";
+ char m58state = '?';
+ int m58k = kill(id(), 0);
+ int m58kerr = errno;
+ FILE* m58f;
+ size_t m58n;
+ snprintf(m58path, sizeof(m58path), "/proc/%d/comm", (int) id());
+ m58f = fopen(m58path, "r");
+ if (m58f) {
+ if (fgets(m58comm, sizeof(m58comm), m58f)) {
+ m58n = strlen(m58comm);
+ if (m58n && m58comm[m58n - 1] == '\n') m58comm[m58n - 1] = 0;
+ }
+ fclose(m58f);
+ }
+ snprintf(m58path, sizeof(m58path), "/proc/%d/stat", (int) id());
+ m58f = fopen(m58path, "r");
+ if (m58f) {
+ char m58buf[512];
+ if (fgets(m58buf, sizeof(m58buf), m58f)) {
+ char* m58p = strrchr(m58buf, ')');
+ if (m58p && m58p[1] == ' ') m58state = m58p[2];
+ }
+ fclose(m58f);
+ }
+ processMemoryAccessLog.error()
+ << "M58DS target hostpid=" << id()
+ << " nsid=" << nsid()
+ << " isDead=" << (isDead() ? 1 : 0)
+ << " kill0=" << m58k
+ << " killerrno=" << m58kerr
+ << " state=" << m58state
+ << " comm=" << m58comm
+ << processMemoryAccessLog.endLog;
+ }
if (errorCode) {
*errorCode = code;
}
@@ -369,7 +417,18 @@ void DarlingServer::Process::notifyCheckin(Architecture architecture) {
} else {
// notify the parent process (if we have one) that we've arrived
if (auto parent = _parentProcess.lock()) {
- dtape_semaphore_up(parent->_dtapeForkWaitSemaphore);
+ /* darling-lol: the parent may already be tearing down (its
+ * fork-wait semaphore destroyed and nullified in
+ * notifyDead/kernelAsync, or across exec). A late checkin
+ * used to up the dead/NULL semaphore -> SIGSEGV *in
+ * darlingserver*, killing the whole container whenever a
+ * child (e.g. a Chromium helper) checked in after its
+ * parent died. Take the parent's lock and skip when gone. */
+ std::unique_lock parentLock(parent->_rwlock);
+ if (parent->_dtapeForkWaitSemaphore != nullptr) {
+ dtape_semaphore_up(parent->_dtapeForkWaitSemaphore);
+ }
+ parentLock.unlock();
parent->_notifyListeningKqchannels(NOTE_FORK, nsid());
}
}
@@ -465,7 +524,106 @@ DarlingServer::Process::MemoryInfo DarlingServer::Process::memoryInfo() const {
return info;
};
-static const std::regex memoryRegionEntryRegex("([0-9a-fA-F]+)\\-([0-9a-fA-F]+)\\s+((?:r|w|x|p|s|\\-)+)\\s+([0-9a-fA-F]+)");
+// parses "start-end perms offset ..." from a `/proc/<pid>/maps` line without std::regex (which is extremely slow here)
+static bool parseMapsLine(const std::string& line, uintptr_t& start, uintptr_t& end, char perms[8], uintptr_t* offset) {
+ unsigned long long s = 0, e = 0, o = 0;
+ char p[8] = {0};
+ if (sscanf(line.c_str(), "%llx-%llx %7s %llx", &s, &e, p, &o) != 4) {
+ return false;
+ }
+ start = s;
+ end = e;
+ memcpy(perms, p, 8);
+ if (offset) {
+ *offset = o;
+ }
+ return true;
+};
+
+// darling-lol: PROCMAP_QUERY (Linux 6.11+) answers "which VMA covers/follows this address" in O(log n)
+// without generating and re-parsing the whole /proc/<pid>/maps text; mach_msg with out-of-line
+// descriptors asked for that on every call and dominated darlingserver CPU. Returns 0 on success,
+// ENOENT when no VMA matches, and -1 when the ioctl is unavailable (callers fall back to the text parser).
+#include <sys/ioctl.h>
+#include <linux/fs.h>
+#include <fcntl.h>
+// Distribution headers older than Linux 6.11 (Ubuntu 24.04 ships 6.8) lack the ioctl; its ABI is
+// fixed, so define it here. On an older running kernel the ioctl fails and the text parser is used.
+#ifndef PROCMAP_QUERY
+#include <linux/types.h>
+enum procmap_query_flags {
+ PROCMAP_QUERY_VMA_READABLE = 0x01,
+ PROCMAP_QUERY_VMA_WRITABLE = 0x02,
+ PROCMAP_QUERY_VMA_EXECUTABLE = 0x04,
+ PROCMAP_QUERY_VMA_SHARED = 0x08,
+ PROCMAP_QUERY_COVERING_OR_NEXT_VMA = 0x10,
+ PROCMAP_QUERY_FILE_BACKED_VMA = 0x20,
+};
+struct procmap_query {
+ __u64 size;
+ __u64 query_flags;
+ __u64 query_addr;
+ __u64 vma_start;
+ __u64 vma_end;
+ __u64 vma_flags;
+ __u64 vma_page_size;
+ __u64 vma_offset;
+ __u64 inode;
+ __u32 dev_major;
+ __u32 dev_minor;
+ __u32 vma_name_size;
+ __u32 build_id_size;
+ __u64 vma_name_addr;
+ __u64 build_id_addr;
+};
+#define PROCMAP_QUERY _IOWR('f', 17, struct procmap_query)
+#endif
+static std::atomic<bool> procmapQueryUnsupported(false);
+
+// Returns 0 on success, ENOENT when no VMA matches, -1 when the ioctl cannot be used (callers fall back to parsing
+// /proc/<pid>/maps text). The descriptor stays open between lookups; any failure other than "no such mapping" drops it
+// and retries once with a fresh one (after an exec the old descriptor refers to a dead address space).
+int DarlingServer::Process::_procmapQuery(uint64_t address, uint64_t flags, void* result) const {
+ if (procmapQueryUnsupported.load(std::memory_order_relaxed)) {
+ return -1;
+ }
+ auto& query = *static_cast<procmap_query*>(result);
+ std::lock_guard<std::mutex> lock(_mapsMutex);
+ for (int attempt = 0; attempt < 2; ++attempt) {
+ bool fresh = false;
+ if (_mapsFd < 0) {
+ _mapsFd = open(("/proc/" + std::to_string(_pid) + "/maps").c_str(), O_RDONLY | O_CLOEXEC);
+ if (_mapsFd < 0) {
+ return -1;
+ }
+ fresh = true;
+ }
+ memset(&query, 0, sizeof(query));
+ query.size = sizeof(query);
+ query.query_flags = flags;
+ query.query_addr = address;
+ int status = ioctl(_mapsFd, PROCMAP_QUERY, &query);
+ int error = errno;
+ if (status == 0) {
+ return 0;
+ }
+ if (error == ENOENT) {
+ return ENOENT;
+ }
+ if (fresh && (error == ENOTTY || error == EINVAL)) {
+ procmapQueryUnsupported.store(true, std::memory_order_relaxed);
+ close(_mapsFd);
+ _mapsFd = -1;
+ return -1;
+ }
+ close(_mapsFd);
+ _mapsFd = -1;
+ if (fresh) {
+ return -1;
+ }
+ }
+ return -1;
+};
DarlingServer::Process::MemoryRegionInfo DarlingServer::Process::memoryRegionInfo(uintptr_t address) const {
if (isDead()) {
@@ -473,31 +631,55 @@ DarlingServer::Process::MemoryRegionInfo DarlingServer::Process::memoryRegionInf
}
MemoryRegionInfo info;
+
+ {
+ procmap_query query;
+ int status = _procmapQuery(address, 0, &query);
+ if (status == 0) {
+ info.startAddress = query.vma_start;
+ info.pageCount = (query.vma_end - query.vma_start) / sysconf(_SC_PAGESIZE);
+ info.mapOffset = query.vma_offset;
+ info.protection = 0;
+ if (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) {
+ info.protection |= PROT_READ;
+ }
+ if (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) {
+ info.protection |= PROT_WRITE;
+ }
+ if (query.vma_flags & PROCMAP_QUERY_VMA_EXECUTABLE) {
+ info.protection |= PROT_EXEC;
+ }
+ info.shared = !!(query.vma_flags & PROCMAP_QUERY_VMA_SHARED);
+ return info;
+ } else if (status == ENOENT) {
+ processLog.warning() << *this << ": Address 0x" << std::hex << address << " not found in \"/proc/" << std::dec << _pid << "/maps\"" << processLog.endLog;
+ throw std::system_error(EFAULT, std::generic_category());
+ }
+ }
+
std::ifstream file("/proc/" + std::to_string(_pid) + "/maps");
std::string line;
uintptr_t endAddress;
while (std::getline(file, line)) {
- std::smatch match;
+ char permsBuffer[8];
+ uintptr_t mapOffset = 0;
- if (!std::regex_search(line, match, memoryRegionEntryRegex)) {
+ if (!parseMapsLine(line, info.startAddress, endAddress, permsBuffer, &mapOffset)) {
processLog.warning() << "Encountered malformed `/proc/<pid>/maps` entry? Definitely a bug (on our part)." << processLog.endLog;
continue;
}
- info.startAddress = std::stoul(match[1].str(), nullptr, 16);
- endAddress = std::stoul(match[2].str(), nullptr, 16);
-
if (endAddress <= address || info.startAddress > address) {
continue;
}
info.pageCount = (endAddress - info.startAddress) / sysconf(_SC_PAGESIZE);
- info.mapOffset = std::stoul(match[4].str(), nullptr, 16);
+ info.mapOffset = mapOffset;
- auto perms = match[3].str();
+ std::string perms = permsBuffer;
info.protection = 0;
@@ -657,27 +839,38 @@ void DarlingServer::Process::syncMemory(uintptr_t address, size_t size, int sync
return thread->syncMemory(address, size, sync_flags);
};
-static const std::regex memoryRegionEntryAddressRegex("([0-9a-fA-F]+)\\-([0-9a-fA-F]+)");
-
uintptr_t DarlingServer::Process::getNextRegion(uintptr_t address) const {
if (isDead()) {
throw std::system_error(ESRCH, std::generic_category(), "dead process; can't call getNextRegion");
}
+ {
+ // first region whose start is above `address`: the VMA covering-or-following address + 1, unless
+ // that one already started at or below `address`, in which case ask again from its end
+ procmap_query query;
+ int status = address == UINTPTR_MAX ? ENOENT : _procmapQuery(address + 1, PROCMAP_QUERY_COVERING_OR_NEXT_VMA, &query);
+ if (status == 0 && query.vma_start <= address) {
+ status = _procmapQuery(query.vma_end, PROCMAP_QUERY_COVERING_OR_NEXT_VMA, &query);
+ }
+ if (status == 0) {
+ return query.vma_start;
+ } else if (status == ENOENT) {
+ return 0;
+ }
+ }
+
std::ifstream file("/proc/" + std::to_string(_pid) + "/maps");
std::string line;
while (std::getline(file, line)) {
- std::smatch match;
+ uintptr_t startAddress, endAddress;
+ char permsBuffer[8];
- if (!std::regex_search(line, match, memoryRegionEntryAddressRegex)) {
+ if (!parseMapsLine(line, startAddress, endAddress, permsBuffer, nullptr)) {
processLog.warning() << "Encountered malformed `/proc/<pid>/maps` entry? Definitely a bug (on our part)." << processLog.endLog;
continue;
}
- auto startAddress = std::stoul(match[1].str(), nullptr, 16);
- auto endAddress = std::stoul(match[2].str(), nullptr, 16);
-
if (startAddress <= address) {
continue;
}
diff --git a/src/server.cpp b/src/server.cpp
index e502e28..8d1cf2b 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -28,6 +28,8 @@
#include <sys/epoll.h>
#include <fcntl.h>
#include <system_error>
+#include <cstdio>
+#include <time.h>
#include <thread>
#include <array>
#include <darlingserver/registry.hpp>
@@ -40,6 +42,44 @@
#include <darlingserver/logging.hpp>
+/* darling-lol M58F129: bounded work-queue trace. With the single-threaded
+ * WorkQueue, this proves whether a process's RPCs wait behind another's: push
+ * (enqueue) and run (dequeue) timestamps per guest thread, in one monotonic
+ * clock, land in dserver.log as `M58WQ push|run pid=.. tid=.. t=<us>`. */
+#include <ctime>
+#include <atomic>
+#include <cstdlib>
+
+static bool m634TraceEnabled() {
+ static const bool enabled = [] {
+ const char* value = std::getenv("DARLING_M634_TRACE");
+ return value && value[0] == '1' && value[1] == '\0';
+ }();
+ return enabled;
+}
+
+static uint64_t m58wq_now(void) {
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (uint64_t)ts.tv_sec * 1000000ull + (uint64_t)(ts.tv_nsec / 1000);
+};
+
+static std::atomic<unsigned long> m58wq_count(0);
+
+static void m58wq_log(const char* what, std::shared_ptr<DarlingServer::Thread> thread) {
+ /* darling-lol M58F151: disabled (per-work-item dserver log overhead) */
+ (void) what;
+ (void) thread;
+#if 0
+ if (m58wq_count.fetch_add(1) >= 200000) return;
+ if (!thread) return;
+ auto proc = thread->process();
+ static const auto log = DarlingServer::Log("M58WQ");
+ log.error() << what << " pid=" << (proc ? proc->id() : -1)
+ << " tid=" << thread->id() << " t=" << m58wq_now() << log.endLog;
+#endif
+};
+
static DarlingServer::Server* sharedInstancePointer = nullptr;
struct DTapeHooks {
@@ -104,8 +144,44 @@ struct DTapeHooks {
std::unique_lock lock(server._timerLock);
- if (!override && server._currentTimerDeadline != 0 && deadline_ns >= server._currentTimerDeadline) {
- return;
+ /* darling-lol M634: bounded trace of timer arms to diagnose the
+ * League-launch wedge (instant semaphore timeouts + RPC storm).
+ * Rate-limited after the boot burst so launch-time behavior persists. */
+ if (m634TraceEnabled()) {
+ static std::atomic<unsigned> m634ArmCount{0};
+ unsigned seen = m634ArmCount.fetch_add(1);
+ if (seen < 120 || seen % 64 == 0) {
+ struct timespec nowTs = {0, 0};
+ clock_gettime(CLOCK_MONOTONIC, &nowTs);
+ uint64_t nowNs = (uint64_t)nowTs.tv_sec * 1000000000ull + nowTs.tv_nsec;
+ fprintf(stderr, "M634ARM line=%u deadline=%llu override=%d prev=%llu now=%llu delta=%lld\n",
+ seen,
+ (unsigned long long)deadline_ns, (int)override,
+ (unsigned long long)server._currentTimerDeadline,
+ (unsigned long long)nowNs,
+ (long long)((int64_t)deadline_ns - (int64_t)nowNs));
+ }
+ }
+
+ if (!override) {
+ /* darling-lol M634: the software marker races with kernel expiry
+ * (an arm landing between kernel expiry and the fire read used to
+ * be skipped, losing that timer forever). Query the kernel's own
+ * remaining time as the source of truth instead: a zero remaining
+ * value means the timer is disarmed/consumed and must be armed. */
+ struct itimerspec armedSpec;
+ if (timerfd_gettime(server._timerFD, &armedSpec) == 0
+ && (armedSpec.it_value.tv_sec != 0 || armedSpec.it_value.tv_nsec != 0)) {
+ struct timespec nowTs = {0, 0};
+ clock_gettime(CLOCK_MONOTONIC, &nowTs);
+ uint64_t armedRemaining = (uint64_t)armedSpec.it_value.tv_sec * 1000000000ull
+ + (uint64_t)armedSpec.it_value.tv_nsec;
+ uint64_t nowNs = (uint64_t)nowTs.tv_sec * 1000000000ull + nowTs.tv_nsec;
+ uint64_t armedDeadline = nowNs + armedRemaining;
+ if (deadline_ns >= armedDeadline) {
+ return;
+ }
+ }
}
server._currentTimerDeadline = deadline_ns;
@@ -115,6 +191,11 @@ struct DTapeHooks {
}
};
+ static int dtape_hook_log_enabled(dtape_log_level_t level) {
+ static const auto log = DarlingServer::Log("dtape");
+ return log.levelEnabled(static_cast<int>(level) + 1) ? 1 : 0;
+ };
+
static void dtape_hook_log(dtape_log_level_t level, const char* message) {
static const auto log = DarlingServer::Log("dtape");
auto process = DarlingServer::Process::currentProcess();
@@ -271,11 +352,23 @@ struct DTapeHooks {
};
static void dtape_hook_task_get_memory_info(void* task_context, dtape_memory_info_t* memory_info) {
- auto info = static_cast<DarlingServer::Process*>(task_context)->memoryInfo();
- memory_info->virtual_size = info.virtualSize;
- memory_info->resident_size = info.residentSize;
- memory_info->page_size = info.pageSize;
- memory_info->region_count = info.regionCount;
+ /* darling-lol M57: a task_info(TASK_BASIC_INFO) for a process that has
+ * already exited made memoryInfo() throw std::system_error, which was
+ * uncaught here and aborted darlingserver, killing the whole session
+ * (Chromium's renderer/GPU probing dies with "dead process; can't call
+ * memoryInfo"). Fail soft with zeroes instead. */
+ try {
+ auto info = static_cast<DarlingServer::Process*>(task_context)->memoryInfo();
+ memory_info->virtual_size = info.virtualSize;
+ memory_info->resident_size = info.residentSize;
+ memory_info->page_size = info.pageSize;
+ memory_info->region_count = info.regionCount;
+ } catch (...) {
+ memory_info->virtual_size = 0;
+ memory_info->resident_size = 0;
+ memory_info->page_size = 0;
+ memory_info->region_count = 0;
+ }
};
static bool dtape_hook_task_get_memory_region_info(void* task_context, uintptr_t address, dtape_memory_region_info_t* memory_region_info) {
@@ -392,6 +485,7 @@ struct DTapeHooks {
.timer_arm = dtape_hook_timer_arm,
.log = dtape_hook_log,
+ .log_enabled = dtape_hook_log_enabled,
.get_load_info = dtape_hook_get_load_info,
.thread_suspend = dtape_hook_thread_suspend,
@@ -445,6 +539,13 @@ DarlingServer::Server::Server(std::string prefix):
{
sharedInstancePointer = this;
+ /* darling-lol: producers send their own replies by default; DSERVER_DIRECT_REPLY=0
+ * restores the old path (every reply wakes the main loop out of epoll_wait). */
+ _directReply = []() {
+ const char* value = std::getenv("DSERVER_DIRECT_REPLY");
+ return !(value && value[0] == '0' && value[1] == '\0');
+ }();
+
// remove the old socket (if it exists)
unlink(_socketPath.c_str());
@@ -540,6 +641,7 @@ void DarlingServer::Server::start() {
// TODO: this could be done concurrently
auto call = DarlingServer::Call::callFromMessage(std::move(*msg));
if (call) {
+ m58wq_log("push", call->thread());
_workQueue.push(call->thread());
}
}
@@ -549,13 +651,15 @@ void DarlingServer::Server::start() {
eventfd_t value;
eventfd_read(_wakeupFD, &value);
- if (_canWrite) {
+ if (_canWrite || !_outbox.empty()) {
_canWrite = _outbox.sendMany(_listenerSocket);
}
struct epoll_event settings;
settings.data.ptr = &_wakeupFD;
- settings.events = (_canWrite) ? (EPOLLIN | EPOLLONESHOT) : 0;
+ // keep the wakeup armed while replies may still be waiting for the socket
+ // to become writable again, so a producer's failed direct send is retried
+ settings.events = (_canWrite || !_outbox.empty()) ? (EPOLLIN | EPOLLONESHOT) : 0;
if (epoll_ctl(_epollFD, EPOLL_CTL_MOD, _wakeupFD, &settings) < 0) {
throw std::system_error(errno, std::generic_category(), "Failed to modify eventfd in epoll context");
@@ -604,6 +708,25 @@ void DarlingServer::Server::start() {
continue;
}
+ /* darling-lol M634: the timer is now consumed; drop the stale
+ * armed-deadline marker so future arms are not skipped. Also
+ * trace the fire (rate-limited) to diagnose the launch wedge. */
+ if (m634TraceEnabled()) {
+ static std::atomic<unsigned> m634FireCount{0};
+ unsigned seen = m634FireCount.fetch_add(1);
+ if (seen < 80 || seen % 64 == 0) {
+ struct timespec nowTs = {0, 0};
+ clock_gettime(CLOCK_MONOTONIC, &nowTs);
+ uint64_t nowNs = (uint64_t)nowTs.tv_sec * 1000000000ull + nowTs.tv_nsec;
+ fprintf(stderr, "M634FIRE line=%u exp=%llu prevDeadline=%llu now=%llu\n",
+ seen,
+ (unsigned long long)expirations,
+ (unsigned long long)_currentTimerDeadline,
+ (unsigned long long)nowNs);
+ }
+ }
+ _currentTimerDeadline = 0;
+
// we're done handling the timerfd;
// we don't need to lock anymore (and the following call might need to arm the timer again)
lock.unlock();
@@ -681,6 +804,7 @@ std::string DarlingServer::Server::prefix() const {
};
void DarlingServer::Server::_worker(std::shared_ptr<Thread> thread) {
+ m58wq_log("run", thread);
thread->doWork();
};
@@ -815,5 +939,20 @@ std::shared_ptr<DarlingServer::FD> DarlingServer::Monitor::fd() const {
};
void DarlingServer::Server::sendMessage(Message&& message) {
- _outbox.push(std::move(message));
+ if (!_directReply) {
+ _outbox.push(std::move(message));
+ return;
+ }
+
+ /* darling-lol: the thread that produced the message usually sends it itself,
+ * instead of waking the main loop for a second pass through its event loop.
+ * pushAndSend only ever sends from the head of the outbox, so ordering is the
+ * same as the main loop's, and it wakes the main loop when the socket is full. */
+ try {
+ _outbox.pushAndSend(std::move(message), _listenerSocket);
+ } catch (const std::system_error&) {
+ /* pushAndSend has already left the message queued and woken the main loop,
+ * where unexpected send errors have always been reported; a worker
+ * microthread must not throw. */
+ }
};